diff --git a/NEWS b/NEWS index 4450776530c4..5de565c78c72 100644 --- a/NEWS +++ b/NEWS @@ -80,6 +80,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..d1d32e8ce2a9 --- /dev/null +++ b/ext/standard/tests/streams/persistent_stream_context_shutdown.phpt @@ -0,0 +1,62 @@ +--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; + + public function stream_open($path, $mode, $options, &$opened_path): bool + { + return true; + } + + public function stream_close(): void + { + $context = stream_context_create([ + 'socket' => ['tcp_nodelay' => true], + ]); + $GLOBALS['late_persistent_client'] = stream_socket_client( + $GLOBALS['late_context_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"); +} + +$GLOBALS['late_context_address'] = 'tcp://' . stream_socket_get_name($server, false); +stream_wrapper_register('late-context', LateContextWrapper::class); +$trigger = fopen('late-context://trigger', 'r'); + +?> +--EXPECT-- +int(0) diff --git a/main/streams/streams.c b/main/streams/streams.c index 368de1a64774..90a6e1cda88e 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -81,14 +81,37 @@ fprintf(stderr, "forget_persistent: %s:%p\n", stream->ops->label, stream); stream->res = NULL; - if (stream->ctx) { - zend_list_delete(stream->ctx); - stream->ctx = NULL; + zend_resource *context = stream->ctx; + stream->ctx = NULL; + if (context) { + zend_list_delete(context); } return 0; } +/* Detach persistent streams that still reference a context being destroyed. */ +static ZEND_ATTRIBUTE_NONNULL void detach_persistent_streams_from_context( + HashTable *persistent_list, zend_resource *context) +{ + zval *el; + + ZEND_HASH_FOREACH_VAL(persistent_list, el) { + ZEND_ASSERT(Z_TYPE_P(el) == IS_RESOURCE); + zend_resource *rsrc = Z_RES_P(el); + + if (rsrc->type == le_pstream) { + php_stream *stream = (php_stream *) rsrc->ptr; + + if (stream->ctx == context) { + stream->res = NULL; + stream->ctx = NULL; + GC_DELREF(context); + } + } + } ZEND_HASH_FOREACH_END(); +} + PHP_RSHUTDOWN_FUNCTION(streams) { zval *el; @@ -2410,6 +2433,10 @@ PHPAPI void php_stream_notification_notify(php_stream_context *context, int noti PHPAPI void php_stream_context_free(php_stream_context *context) { + if ((EG(flags) & EG_FLAGS_IN_RESOURCE_SHUTDOWN) && context->res != NULL) { + detach_persistent_streams_from_context(&EG(persistent_list), context->res); + } + if (Z_TYPE(context->options) != IS_UNDEF) { zval_ptr_dtor(&context->options); ZVAL_UNDEF(&context->options);