From 9c460bb7b0a5fddbfc38445789249a7beee9fa0a Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Fri, 25 Sep 2026 14:53:03 -0600 Subject: [PATCH 1/2] Add persistent stream context shutdown reproducer --- NEWS | 1 + .../persistent_stream_context_shutdown.phpt | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 ext/standard/tests/streams/persistent_stream_context_shutdown.phpt diff --git a/NEWS b/NEWS index bde051dee660..29e7b685a0d5 100644 --- a/NEWS +++ b/NEWS @@ -113,6 +113,7 @@ PHP NEWS . Fixed three Windows-only proc_open() defects: an uninitialized PROCESS_INFORMATION, an indeterminate comspec pointer after a failed lookup, and an unchecked CreateFileA() failure. (Ilia Alshanetsky) + . Fix persistent stream context lifetime during shutdown (Levi Morrison) - XSL: . Fixed bug GH-23730 (use-after-free when XSLTProcessor::importStylesheet() diff --git a/ext/standard/tests/streams/persistent_stream_context_shutdown.phpt b/ext/standard/tests/streams/persistent_stream_context_shutdown.phpt new file mode 100644 index 000000000000..34a0b42a9f45 --- /dev/null +++ b/ext/standard/tests/streams/persistent_stream_context_shutdown.phpt @@ -0,0 +1,63 @@ +--TEST-- +Persistent stream contexts created during resource shutdown are detached +--FILE-- + ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ], + $pipes, + ); + fclose($pipes[0]); + stream_get_contents($pipes[1]); + fclose($pipes[1]); + stream_get_contents($pipes[2]); + fclose($pipes[2]); + var_dump(proc_close($process)); + return; +} + +final class LateContextWrapper +{ + public $context; + private string $address; + + public function stream_open($path, $mode, $options, &$opened_path): bool + { + $this->address = substr($path, strlen('late-context://')); + return true; + } + + public function stream_close(): void + { + $context = stream_context_create([ + 'socket' => ['tcp_nodelay' => true], + ]); + stream_socket_client( + $this->address, + $errno, + $error, + 1, + STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT, + $context, + ); + } +} + +$server = stream_socket_server('tcp://127.0.0.1:0', $errno, $error); +if (!$server) { + die("server failed: $error ($errno)\n"); +} + +stream_wrapper_register('late-context', LateContextWrapper::class); +$trigger = fopen('late-context://tcp://' . stream_socket_get_name($server, false), 'r'); + +?> +--EXPECT-- +int(0) From 06dda61566597abc512a16ffbcd8dd433a2e7f6d Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Fri, 25 Sep 2026 16:37:05 -0600 Subject: [PATCH 2/2] Clear persistent stream request pointers after shutdown --- ext/standard/basic_functions.c | 5 +++-- main/php_streams.h | 2 +- main/streams/streams.c | 9 +++------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index a1bf86d0bcfa..ae1a1ef8666f 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -157,7 +157,9 @@ zend_module_entry basic_functions_module = { /* {{{ */ PHP_RSHUTDOWN(basic), /* request shutdown */ PHP_MINFO(basic), /* extension info */ PHP_STANDARD_VERSION, /* extension version */ - STANDARD_MODULE_PROPERTIES + NO_MODULE_GLOBALS, + ZEND_MODULE_POST_ZEND_DEACTIVATE_N(streams), + STANDARD_MODULE_PROPERTIES_EX }; /* }}} */ @@ -463,7 +465,6 @@ PHP_RSHUTDOWN_FUNCTION(basic) /* {{{ */ #endif BASIC_RSHUTDOWN_SUBMODULE(assert) BASIC_RSHUTDOWN_SUBMODULE(url_scanner_ex) - BASIC_RSHUTDOWN_SUBMODULE(streams) #ifdef PHP_WIN32 BASIC_RSHUTDOWN_SUBMODULE(win32_core_globals) #endif diff --git a/main/php_streams.h b/main/php_streams.h index 1c4141c939f1..fc1585ed7139 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -586,7 +586,7 @@ END_EXTERN_C() int php_init_stream_wrappers(int module_number); void php_shutdown_stream_wrappers(int module_number); void php_shutdown_stream_hashes(void); -PHP_RSHUTDOWN_FUNCTION(streams); +ZEND_MODULE_POST_ZEND_DEACTIVATE_D(streams); BEGIN_EXTERN_C() PHPAPI zend_result php_register_url_stream_wrapper(const char *protocol, const php_stream_wrapper *wrapper); diff --git a/main/streams/streams.c b/main/streams/streams.c index 368de1a64774..a37546a4ad2b 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -79,17 +79,14 @@ static int forget_persistent_resource_id_numbers(zval *el) fprintf(stderr, "forget_persistent: %s:%p\n", stream->ops->label, stream); #endif + /* Request resources have been destroyed; clear their stale pointers. */ stream->res = NULL; - - if (stream->ctx) { - zend_list_delete(stream->ctx); - stream->ctx = NULL; - } + stream->ctx = NULL; return 0; } -PHP_RSHUTDOWN_FUNCTION(streams) +ZEND_MODULE_POST_ZEND_DEACTIVATE_D(streams) { zval *el;