Commit ff03f03b826 for php.net
commit ff03f03b8264f9acf9ff06a12b2970987e4685e3
Author: Jakub Zelenka <bukka@php.net>
Date: Tue Sep 15 18:25:26 2026 +0200
main/streams: Fix parent lookup when unwinding stream error operations
diff --git a/ext/standard/tests/streams/stream_errors_nested_operation_unwind.phpt b/ext/standard/tests/streams/stream_errors_nested_operation_unwind.phpt
new file mode 100644
index 00000000000..13cd22b01d5
--- /dev/null
+++ b/ext/standard/tests/streams/stream_errors_nested_operation_unwind.phpt
@@ -0,0 +1,83 @@
+--TEST--
+Stream errors: nested operation unwinding restores the parent operation
+--FILE--
+<?php
+class ErrorStream
+{
+ public $context;
+
+ public function stream_open($path, $mode, $options, &$openedPath): bool
+ {
+ return true;
+ }
+
+ public function stream_read(int $count): string
+ {
+ return str_repeat('A', $count + 1);
+ }
+
+ public function stream_eof(): bool
+ {
+ return true;
+ }
+
+ public function stream_stat(): array
+ {
+ return [];
+ }
+}
+
+class NestingStream
+{
+ public $context;
+
+ public function stream_open($path, $mode, $options, &$openedPath): bool
+ {
+ return true;
+ }
+
+ public function stream_read(int $count): string
+ {
+ $inner = fopen('php://memory', 'r');
+ fread($inner, 1);
+ fclose($inner);
+ return 'x';
+ }
+
+ public function stream_eof(): bool
+ {
+ return true;
+ }
+
+ public function stream_stat(): array
+ {
+ return [];
+ }
+}
+
+stream_wrapper_register('error-stream', ErrorStream::class);
+stream_wrapper_register('nesting-stream', NestingStream::class);
+
+$busy = false;
+set_error_handler(static function (int $severity, string $message) use (&$busy): bool {
+ echo "handler: $message\n";
+ if ($busy) {
+ return true;
+ }
+ $busy = true;
+ $s = fopen('nesting-stream://x', 'r');
+ fread($s, 1);
+ fclose($s);
+ $busy = false;
+ return true;
+});
+
+$stream = fopen('error-stream://x', 'r');
+var_dump(fread($stream, 1));
+fclose($stream);
+echo "done\n";
+?>
+--EXPECT--
+handler: fread(): ErrorStream::stream_read - read 1 bytes more data than requested (8193 read, 8192 max) - excess data will be lost
+string(1) "A"
+done
diff --git a/ext/standard/tests/streams/stream_errors_wrapper_nested_operation.phpt b/ext/standard/tests/streams/stream_errors_wrapper_nested_operation.phpt
new file mode 100644
index 00000000000..383cb56a6b0
--- /dev/null
+++ b/ext/standard/tests/streams/stream_errors_wrapper_nested_operation.phpt
@@ -0,0 +1,94 @@
+--TEST--
+Stream errors: wrapper errors after a nested stream operation are still reported
+--FILE--
+<?php
+class ErrorStream
+{
+ public $context;
+
+ public function stream_open($path, $mode, $options, &$openedPath): bool
+ {
+ return true;
+ }
+
+ public function stream_read(int $count): string
+ {
+ return str_repeat('A', $count + 1);
+ }
+
+ public function stream_eof(): bool
+ {
+ return true;
+ }
+
+ public function stream_stat(): array
+ {
+ return [];
+ }
+}
+
+class ProxyStream
+{
+ public $context;
+ private int $reads = 0;
+
+ public function stream_open($path, $mode, $options, &$openedPath): bool
+ {
+ return true;
+ }
+
+ public function stream_read(int $count): string
+ {
+ $this->reads++;
+ if ($this->reads === 2) {
+ $inner = fopen('error-stream://x', 'r', false, $GLOBALS['innerCtx']);
+ fread($inner, 1);
+ fclose($inner);
+ }
+ return $this->reads <= 2 ? str_repeat('B', $count + 1) : '';
+ }
+
+ public function stream_eof(): bool
+ {
+ return $this->reads >= 3;
+ }
+
+ public function stream_stat(): array
+ {
+ return [];
+ }
+}
+
+stream_wrapper_register('error-stream', ErrorStream::class);
+stream_wrapper_register('proxy', ProxyStream::class);
+
+$innerCtx = stream_context_create(['stream' => [
+ 'error_mode' => StreamErrorMode::Silent,
+ 'error_handler' => static function (array $errors): void {
+ echo "inner handler: " . count($errors) . " error(s): {$errors[0]->code->name}\n";
+ },
+]]);
+
+$outerCtx = stream_context_create(['stream' => [
+ 'error_mode' => StreamErrorMode::Silent,
+ 'error_store' => StreamErrorStore::All,
+ 'error_handler' => static function (array $errors): void {
+ echo "outer handler: " . count($errors) . " error(s)\n";
+ foreach ($errors as $error) {
+ echo " {$error->code->name}: {$error->message}\n";
+ }
+ },
+]]);
+
+$stream = fopen('proxy://x', 'r', false, $outerCtx);
+var_dump(strlen(stream_get_contents($stream)));
+fclose($stream);
+var_dump(count(stream_last_errors()));
+?>
+--EXPECT--
+inner handler: 1 error(s): UserspaceInvalidReturn
+outer handler: 2 error(s)
+ UserspaceInvalidReturn: ProxyStream::stream_read - read 1 bytes more data than requested (8193 read, 8192 max) - excess data will be lost
+ UserspaceInvalidReturn: ProxyStream::stream_read - read 1 bytes more data than requested (8193 read, 8192 max) - excess data will be lost
+int(16384)
+int(2)
diff --git a/main/streams/stream_errors.c b/main/streams/stream_errors.c
index 73f3313ce1e..94a0b5692ee 100644
--- a/main/streams/stream_errors.c
+++ b/main/streams/stream_errors.c
@@ -207,11 +207,11 @@ static inline php_stream_error_operation *php_stream_get_parent_operation(void)
{
const php_stream_error_state *state = &FG(stream_error_state);
- if (state->operation_depth <= 1) {
+ if (state->operation_depth < 1) {
return NULL;
}
- return php_stream_get_operation_at_depth(state->operation_depth - 2);
+ return php_stream_get_operation_at_depth(state->operation_depth - 1);
}
/* Clean up functions */