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);