From 7e54d82003f84289bcdf2e39c125802e9775f667 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 20 Sep 2026 21:50:34 +0200 Subject: [PATCH] Fix use-after-free in StreamPollHandle when its stream is closed StreamPollHandle takes a reference on the resource but caches the php_stream pointer it was given at construction. fclose() frees that stream while the resource stays alive, so Watcher::remove(), Watcher::modifyEvents() and StreamPollHandle::isValid() dereference freed memory. Resolve the stream from the resource instead: res->ptr is NULL once the stream is closed, which the existing SOCK_ERR paths already handle. --- ext/standard/io_poll.c | 31 +++++++++++----- .../poll_stream_handle_close_then_use.phpt | 37 +++++++++++++++++++ 2 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 ext/standard/tests/poll/poll_stream_handle_close_then_use.phpt diff --git a/ext/standard/io_poll.c b/ext/standard/io_poll.c index a8a0563627fa..72f1be309ccd 100644 --- a/ext/standard/io_poll.c +++ b/ext/standard/io_poll.c @@ -67,7 +67,6 @@ struct php_io_poll_context_object { /* Stream poll handle specific data */ typedef struct php_stream_poll_handle_data { - php_stream *stream; zend_resource *res; } php_stream_poll_handle_data; @@ -180,16 +179,29 @@ static const char *php_io_poll_backend_type_to_name(php_poll_backend_type type) /* Stream Poll Handle Implementation */ -static php_socket_t php_stream_poll_handle_get_fd(php_poll_handle_object *handle) +static php_stream *php_stream_poll_handle_get_stream(php_poll_handle_object *handle) { php_stream_poll_handle_data *data = handle->handle_data; + + /* The reference taken on the resource keeps it alive, but fclose() may have + * destroyed the stream it points to in the meantime. */ + if (!data || !data->res || data->res->ptr == NULL) { + return NULL; + } + + return (php_stream *) data->res->ptr; +} + +static php_socket_t php_stream_poll_handle_get_fd(php_poll_handle_object *handle) +{ + php_stream *stream = php_stream_poll_handle_get_stream(handle); php_socket_t fd; - if (!data || !data->stream) { + if (!stream) { return SOCK_ERR; } - if (php_stream_cast(data->stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, + if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void *) &fd, 1) != SUCCESS || fd == -1) { @@ -201,8 +213,8 @@ static php_socket_t php_stream_poll_handle_get_fd(php_poll_handle_object *handle static int php_stream_poll_handle_is_valid(php_poll_handle_object *handle) { - php_stream_poll_handle_data *data = handle->handle_data; - return data && data->stream && !php_stream_eof(data->stream); + php_stream *stream = php_stream_poll_handle_get_stream(handle); + return stream && !php_stream_eof(stream); } static void php_stream_poll_handle_cleanup(php_poll_handle_object *handle) @@ -454,7 +466,6 @@ PHP_METHOD(StreamPollHandle, __construct) /* Set up stream-specific data */ php_stream_poll_handle_data *data = emalloc(sizeof(php_stream_poll_handle_data)); - data->stream = stream; data->res = stream->res; intern->handle_data = data; @@ -469,12 +480,12 @@ PHP_METHOD(StreamPollHandle, getStream) php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(getThis()); php_stream_poll_handle_data *data = intern->handle_data; - if (!data || !data->stream) { + if (!data || !data->res) { RETURN_NULL(); } - GC_ADDREF(data->stream->res); - php_stream_to_zval(data->stream, return_value); + GC_ADDREF(data->res); + RETURN_RES(data->res); } PHP_METHOD(StreamPollHandle, isValid) diff --git a/ext/standard/tests/poll/poll_stream_handle_close_then_use.phpt b/ext/standard/tests/poll/poll_stream_handle_close_then_use.phpt new file mode 100644 index 000000000000..d317e79577e0 --- /dev/null +++ b/ext/standard/tests/poll/poll_stream_handle_close_then_use.phpt @@ -0,0 +1,37 @@ +--TEST-- +Io\Poll: StreamPollHandle operations are safe when the stream is closed first +--FILE-- +add($handle, [Io\Poll\Event::Read]); + +fclose($r); + +var_dump($handle->isValid()); +var_dump(is_resource($handle->getStream())); + +try { + $watcher->modifyEvents([Io\Poll\Event::Read, Io\Poll\Event::Write]); +} catch (Io\Poll\InvalidHandleException $e) { + echo $e->getMessage(), "\n"; +} + +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; + +$watcher->remove(); +var_dump($watcher->isActive()); + +fclose($w); +echo "ok\n"; +?> +--EXPECT-- +bool(false) +bool(false) +Invalid handle for polling +Events count: 0 +bool(false) +ok