Commit ee3035bc660 for php
commit ee3035bc66040d6fe400d1731b6a4b6b9b55e195
Author: Levi Morrison <levi.morrison@datadoghq.com>
Date: Mon Sep 28 07:28:42 2026 -0600
Fix persistent stream context lifetime during shutdown (#23853)
* Add persistent stream context shutdown reproducer
* Clear persistent stream request pointers after shutdown
diff --git a/NEWS b/NEWS
index 686eedc4645..4dedb89b0d3 100644
--- a/NEWS
+++ b/NEWS
@@ -115,6 +115,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/basic_functions.c b/ext/standard/basic_functions.c
index a1bf86d0bcf..ae1a1ef8666 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/ext/standard/tests/streams/persistent_stream_context_shutdown.phpt b/ext/standard/tests/streams/persistent_stream_context_shutdown.phpt
new file mode 100644
index 00000000000..34a0b42a9f4
--- /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--
+<?php
+
+if (($argv[1] ?? null) !== 'child') {
+ // Wait for the trigger's final status because the crash occurs after stdout closes.
+ $process = proc_open(
+ [PHP_BINARY, '-n', __FILE__, 'child'],
+ [
+ 0 => ['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)
diff --git a/main/php_streams.h b/main/php_streams.h
index 1c4141c939f..fc1585ed713 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 368de1a6477..a37546a4ad2 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;