Commit a5462199410 for php.net

commit a5462199410be94820a8f7abde6322024d9a1ed1
Author: Jakub Zelenka <bukka@php.net>
Date:   Tue Sep 15 13:42:15 2026 +0200

    main/streams: Keep context alive during stream error finalization

    Fixes GH-23259
    Fixes GH-23264

diff --git a/ext/phar/tests/gh23259.phpt b/ext/phar/tests/gh23259.phpt
new file mode 100644
index 00000000000..b71ff9c756c
--- /dev/null
+++ b/ext/phar/tests/gh23259.phpt
@@ -0,0 +1,36 @@
+--TEST--
+GH-23259 (Stream-error finalization uses streams after close)
+--EXTENSIONS--
+phar
+--INI--
+phar.readonly=0
+--FILE--
+<?php
+$directory = __DIR__ . '/gh23259';
+$archive = $directory . '/close-error.phar';
+
+mkdir($directory);
+$phar = new Phar($archive);
+$phar['seed.txt'] = 'seed';
+unset($phar);
+
+$context = stream_context_create(['stream' => ['error_mode' => StreamErrorMode::Error]]);
+$stream = fopen("phar://$archive/new.txt", 'wb', false, $context);
+unset($context);
+fwrite($stream, str_repeat('A', 4096));
+
+unlink($archive);
+
+var_dump(fclose($stream));
+?>
+--CLEAN--
+<?php
+$directory = __DIR__ . '/gh23259';
+@unlink($directory . '/close-error.phar');
+@rmdir($directory);
+?>
+--EXPECTF--
+Warning: fclose(): unable to seek to start of file "seed.txt" while creating new phar "%s" in %s on line %d
+
+Warning: fclose(): unable to seek to start of file "seed.txt" while creating new phar "%s" in %s on line %d
+bool(false)
diff --git a/ext/phar/tests/gh23259_pclose.phpt b/ext/phar/tests/gh23259_pclose.phpt
new file mode 100644
index 00000000000..585b588219c
--- /dev/null
+++ b/ext/phar/tests/gh23259_pclose.phpt
@@ -0,0 +1,36 @@
+--TEST--
+GH-23259 (Stream-error finalization uses streams after close) - pclose()
+--EXTENSIONS--
+phar
+--INI--
+phar.readonly=0
+--FILE--
+<?php
+$directory = __DIR__ . '/gh23259_pclose';
+$archive = $directory . '/close-error.phar';
+
+mkdir($directory);
+$phar = new Phar($archive);
+$phar['seed.txt'] = 'seed';
+unset($phar);
+
+$context = stream_context_create(['stream' => ['error_mode' => StreamErrorMode::Error]]);
+$stream = fopen("phar://$archive/new.txt", 'wb', false, $context);
+unset($context);
+fwrite($stream, str_repeat('A', 4096));
+
+unlink($archive);
+
+var_dump(pclose($stream));
+?>
+--CLEAN--
+<?php
+$directory = __DIR__ . '/gh23259_pclose';
+@unlink($directory . '/close-error.phar');
+@rmdir($directory);
+?>
+--EXPECTF--
+Warning: pclose(): unable to seek to start of file "seed.txt" while creating new phar "%s" in %s on line %d
+
+Warning: pclose(): unable to seek to start of file "seed.txt" while creating new phar "%s" in %s on line %d
+int(-1)
diff --git a/ext/standard/file.c b/ext/standard/file.c
index 19e60a41b05..f33f1585dbf 100644
--- a/ext/standard/file.c
+++ b/ext/standard/file.c
@@ -366,9 +366,16 @@ PHP_FUNCTION(get_meta_tags)

 	if (value) efree(value);
 	if (name) efree(name);
-	php_stream_close(md.stream);

-	php_stream_error_operation_end_for_stream(md.stream);
+	php_stream_context *context = PHP_STREAM_CONTEXT(md.stream);
+	if (context) {
+		GC_ADDREF(context->res);
+	}
+	php_stream_close(md.stream);
+	php_stream_error_operation_end(context);
+	if (context) {
+		zend_list_delete(context->res);
+	}
 }
 /* }}} */

@@ -775,11 +782,18 @@ PHPAPI PHP_FUNCTION(fclose)
 		RETURN_FALSE;
 	}

+	php_stream_context *context = PHP_STREAM_CONTEXT(stream);
+	if (context) {
+		GC_ADDREF(context->res);
+	}
 	php_stream_error_operation_begin();
 	int free_result = php_stream_free(stream,
 		PHP_STREAM_FREE_KEEP_RSRC |
 		(stream->is_persistent ? PHP_STREAM_FREE_CLOSE_PERSISTENT : PHP_STREAM_FREE_CLOSE));
-	php_stream_error_operation_end_for_stream(stream);
+	php_stream_error_operation_end(context);
+	if (context) {
+		zend_list_delete(context->res);
+	}

 	RETURN_BOOL(!free_result);
 }
@@ -851,11 +865,18 @@ PHP_FUNCTION(pclose)
 		PHP_Z_PARAM_STREAM(stream)
 	ZEND_PARSE_PARAMETERS_END();

+	php_stream_context *context = PHP_STREAM_CONTEXT(stream);
+	if (context) {
+		GC_ADDREF(context->res);
+	}
 	php_stream_error_operation_begin();
 	FG(pclose_wait) = 1;
 	zend_list_close(stream->res);
 	FG(pclose_wait) = 0;
-	php_stream_error_operation_end_for_stream(stream);
+	php_stream_error_operation_end(context);
+	if (context) {
+		zend_list_delete(context->res);
+	}
 	RETURN_LONG(FG(pclose_ret));
 }
 /* }}} */
diff --git a/ext/standard/tests/streams/gh23259.phpt b/ext/standard/tests/streams/gh23259.phpt
new file mode 100644
index 00000000000..9668842edd2
--- /dev/null
+++ b/ext/standard/tests/streams/gh23259.phpt
@@ -0,0 +1,38 @@
+--TEST--
+GH-23259 (Stream-error finalization uses streams after close)
+--FILE--
+<?php
+class OversizedReadStream
+{
+    public $context;
+    private bool $read = false;
+
+    public function stream_open($path, $mode, $options, &$openedPath): bool
+    {
+        return true;
+    }
+
+    public function stream_read(int $count): string
+    {
+        $this->read = true;
+        return str_repeat('A', $count + 1);
+    }
+
+    public function stream_eof(): bool
+    {
+        return $this->read;
+    }
+
+    public function stream_stat(): array
+    {
+        return [];
+    }
+}
+
+stream_wrapper_register('oversized-read', OversizedReadStream::class);
+var_dump(get_meta_tags('oversized-read://input'));
+?>
+--EXPECTF--
+Warning: get_meta_tags(): OversizedReadStream::stream_read - read 1 bytes more data than requested (8193 read, 8192 max) - excess data will be lost in %s on line %d
+array(0) {
+}
diff --git a/ext/standard/tests/streams/gh23264.phpt b/ext/standard/tests/streams/gh23264.phpt
new file mode 100644
index 00000000000..449e35a7b7c
--- /dev/null
+++ b/ext/standard/tests/streams/gh23264.phpt
@@ -0,0 +1,54 @@
+--TEST--
+GH-23264 (Streams: set_error_handler() callback frees the structured-handler context)
+--FILE--
+<?php
+class OversizedReadStream
+{
+    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 false;
+    }
+
+    public function stream_stat(): array
+    {
+        return [];
+    }
+}
+
+stream_wrapper_register('oversized-read', OversizedReadStream::class);
+
+$context = stream_context_create([
+    'stream' => [
+        'error_mode' => StreamErrorMode::Error,
+        'error_handler' => static function (array $errors): void {
+            echo "handler: {$errors[0]->code->name}\n";
+        },
+    ],
+]);
+$stream = fopen('oversized-read://input', 'r', false, $context);
+unset($context);
+
+set_error_handler(static function (int $severity, string $message) use (&$stream): bool {
+    echo "legacy: $message\n";
+    fclose($stream);
+    return true;
+});
+
+var_dump(fread($stream, 1));
+?>
+--EXPECT--
+legacy: fread(): OversizedReadStream::stream_read - read 1 bytes more data than requested (8193 read, 8192 max) - excess data will be lost
+handler: UserspaceInvalidReturn
+string(1) "A"
diff --git a/main/streams/stream_errors.c b/main/streams/stream_errors.c
index 4e59e275cf9..73f3313ce1e 100644
--- a/main/streams/stream_errors.c
+++ b/main/streams/stream_errors.c
@@ -460,7 +460,13 @@ PHPAPI void php_stream_error_operation_end(const php_stream_context *context)

 		bool is_terminating = php_stream_has_terminating_error(op);

+		if (context) {
+			GC_ADDREF(context->res);
+		}
 		php_stream_report_errors(context, op, error_mode, is_terminating);
+		if (context) {
+			zend_list_delete(context->res);
+		}

 		if (store_mode == PHP_STREAM_ERROR_STORE_NONE) {
 			php_stream_error_entry_free(op->first_error);