From 00ea746c74e8466ae406c65b33865082426a3caa Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 21 Sep 2026 14:51:35 +0200 Subject: [PATCH] Fix GH-22844: Io\Poll watchers after the watched stream is closed A StreamPollHandle keeps a reference to the stream resource, but an explicit fclose() still closes the stream underneath the watcher. The watcher could then no longer resolve its fd, so its backend registration was left behind: a recycled fd number confused the registry and the poll backend, and on epoll a duplicated fd could keep the interest alive and hand a freed watcher back from wait(). Streams now keep a list of their watchers and notify them from php_stream_free() before the fd is closed, so every watcher is unregistered while the fd is still valid. The Context registry is keyed by the registered fd, which the watcher caches, so remove() no longer depends on the stream and an fd number reused after a close is detected on add(). A watcher retired by the close reports inactive and its remove() becomes a no-op. modifyEvents() re-adds a fired one-shot registration that the backend dropped, and the kqueue backend drops its tracking entry when there is nothing left to delete. This follows the approach of GH-22848 by Ilia Alshanetsky. --- ext/standard/io_poll.c | 166 +++++++++++++----- ext/standard/io_poll.h | 26 +++ .../tests/poll/poll_stream_closed_dup_fd.phpt | 49 ++++++ .../poll/poll_stream_closed_fd_reuse.phpt | 47 +++++ ...poll_stream_closed_fd_reuse_unwatched.phpt | 36 ++++ .../tests/poll/poll_stream_closed_many.phpt | 52 ++++++ .../poll_stream_closed_multi_context.phpt | 34 ++++ .../poll/poll_stream_closed_persistent.phpt | 28 +++ .../poll_stream_closed_watcher_inactive.phpt | 38 ++++ .../poll_stream_handle_closed_stream.phpt | 4 +- .../tests/poll/poll_stream_oneshot_rearm.phpt | 44 +++++ main/php_streams.h | 2 + main/poll/poll_backend_kqueue.c | 10 +- main/streams/streams.c | 10 ++ 14 files changed, 496 insertions(+), 50 deletions(-) create mode 100644 ext/standard/io_poll.h create mode 100644 ext/standard/tests/poll/poll_stream_closed_dup_fd.phpt create mode 100644 ext/standard/tests/poll/poll_stream_closed_fd_reuse.phpt create mode 100644 ext/standard/tests/poll/poll_stream_closed_fd_reuse_unwatched.phpt create mode 100644 ext/standard/tests/poll/poll_stream_closed_many.phpt create mode 100644 ext/standard/tests/poll/poll_stream_closed_multi_context.phpt create mode 100644 ext/standard/tests/poll/poll_stream_closed_persistent.phpt create mode 100644 ext/standard/tests/poll/poll_stream_closed_watcher_inactive.phpt create mode 100644 ext/standard/tests/poll/poll_stream_oneshot_rearm.phpt diff --git a/ext/standard/io_poll.c b/ext/standard/io_poll.c index 5632fc270c56..dd0b27ae0067 100644 --- a/ext/standard/io_poll.c +++ b/ext/standard/io_poll.c @@ -17,6 +17,7 @@ #include "zend_exceptions.h" #include "php_network.h" #include "php_poll.h" +#include "io_poll.h" #include "io_poll_arginfo.h" #include "io_poll_decl.h" #include "ext/date/php_time.h" @@ -54,14 +55,17 @@ typedef struct php_io_poll_watcher_object { uint32_t triggered_events; zval data; bool active; + bool closed; /* Deactivated because its stream was closed */ php_io_poll_context_object *context; /* Back reference to Context object */ + php_socket_t fd; /* Registered fd, SOCK_ERR when inactive */ + php_stream *stream; /* Watched stream, NULL when not registered in its watcher list */ zend_object std; } php_io_poll_watcher_object; /* Context object structure */ struct php_io_poll_context_object { php_poll_ctx *ctx; - HashTable *watchers; /* Maps handle pointer -> watcher object */ + HashTable *watchers; /* Maps fd -> watcher object */ zend_object std; }; @@ -266,7 +270,10 @@ static zend_object *php_io_poll_watcher_create_object(zend_class_entry *ce) intern->watched_events = 0; intern->triggered_events = 0; intern->active = false; + intern->closed = false; intern->context = NULL; + intern->fd = SOCK_ERR; + intern->stream = NULL; ZVAL_NULL(&intern->data); return &intern->std; @@ -285,12 +292,88 @@ static zend_object *php_io_poll_context_create_object(zend_class_entry *ce) return &intern->std; } +/* Watcher registration helpers */ + +static zend_always_inline zend_ulong php_io_poll_compute_ptr_key(void *ptr) +{ + zend_ulong key = (zend_ulong) (uintptr_t) ptr; + return (key >> 3) | (key << ((sizeof(key) * 8) - 3)); +} + +static zend_always_inline void php_io_poll_watcher_deactivate(php_io_poll_watcher_object *watcher) +{ + watcher->active = false; + watcher->context = NULL; + watcher->fd = SOCK_ERR; +} + +static void php_io_poll_stream_watch(php_stream *stream, php_io_poll_watcher_object *watcher) +{ + if (!stream->poll_watchers) { + stream->poll_watchers = pemalloc(sizeof(HashTable), stream->is_persistent); + zend_hash_init(stream->poll_watchers, 4, NULL, NULL, stream->is_persistent); + } + + zval zv; + ZVAL_PTR(&zv, watcher); + zend_hash_index_add_new(stream->poll_watchers, php_io_poll_compute_ptr_key(watcher), &zv); + watcher->stream = stream; +} + +static void php_io_poll_stream_unwatch(php_io_poll_watcher_object *watcher) +{ + php_stream *stream = watcher->stream; + + if (!stream) { + return; + } + watcher->stream = NULL; + + zend_hash_index_del(stream->poll_watchers, php_io_poll_compute_ptr_key(watcher)); + if (zend_hash_num_elements(stream->poll_watchers) == 0) { + zend_hash_destroy(stream->poll_watchers); + pefree(stream->poll_watchers, stream->is_persistent); + stream->poll_watchers = NULL; + } +} + +static void php_io_poll_context_retire_watcher( + php_io_poll_context_object *context, php_io_poll_watcher_object *watcher) +{ + php_socket_t fd = watcher->fd; + + php_poll_remove(context->ctx, (int) fd); + php_io_poll_stream_unwatch(watcher); + php_io_poll_watcher_deactivate(watcher); + zend_hash_index_del(context->watchers, (zend_ulong) fd); +} + +/* Called from php_stream_free() while the fd is still open */ +PHPAPI void php_io_poll_stream_notify_close(php_stream *stream) +{ + HashTable *watchers = stream->poll_watchers; + stream->poll_watchers = NULL; + + ZEND_HASH_FOREACH_VAL(watchers, zval *zv) { + php_io_poll_watcher_object *watcher = Z_PTR_P(zv); + watcher->stream = NULL; + watcher->closed = true; + if (watcher->context) { + php_io_poll_context_retire_watcher(watcher->context, watcher); + } + } ZEND_HASH_FOREACH_END(); + + zend_hash_destroy(watchers); + pefree(watchers, stream->is_persistent); +} + /* Object Destruction Functions */ static void php_io_poll_watcher_free_object(zend_object *obj) { php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(obj); + php_io_poll_stream_unwatch(intern); zval_ptr_dtor(&intern->data); if (intern->handle) { @@ -307,8 +390,8 @@ static void php_io_poll_context_free_object(zend_object *obj) if (intern->watchers) { ZEND_HASH_FOREACH_VAL(intern->watchers, zval *zv) { php_io_poll_watcher_object *watcher = PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(Z_OBJ_P(zv)); - watcher->active = false; - watcher->context = NULL; + php_io_poll_stream_unwatch(watcher); + php_io_poll_watcher_deactivate(watcher); } ZEND_HASH_FOREACH_END(); } @@ -355,12 +438,6 @@ static HashTable *php_io_poll_context_get_gc(zend_object *obj, zval **table, int /* Utility functions */ -static zend_always_inline zend_ulong php_io_poll_compute_ptr_key(void *ptr) -{ - zend_ulong key = (zend_ulong) (uintptr_t) ptr; - return (key >> 3) | (key << ((sizeof(key) * 8) - 3)); -} - static zend_result php_io_poll_watcher_modify_events( php_io_poll_watcher_object *watcher, uint32_t events) { @@ -370,16 +447,12 @@ static zend_result php_io_poll_watcher_modify_events( return FAILURE; } - php_socket_t fd = php_poll_handle_get_fd(watcher->handle); - if (fd == SOCK_ERR) { - zend_throw_exception( - php_io_poll_invalid_handle_class_entry, "Invalid handle for polling", 0); - return FAILURE; - } - - /* Modify in poll context */ + /* Re-add if the backend dropped a fired one-shot registration */ php_poll_ctx *poll_ctx = watcher->context->ctx; - if (php_poll_modify(poll_ctx, (int) fd, events, watcher) != SUCCESS) { + int fd = (int) watcher->fd; + if (php_poll_modify(poll_ctx, fd, events, watcher) != SUCCESS + && (php_poll_get_error(poll_ctx) != PHP_POLL_ERR_NOTFOUND + || php_poll_add(poll_ctx, fd, events, watcher) != SUCCESS)) { php_poll_error err = php_poll_get_error(poll_ctx); php_io_poll_throw_failed_operation(php_io_poll_failed_watcher_mod_class_entry, "Failed to modify watcher in polling system", err); @@ -634,26 +707,16 @@ PHP_METHOD(Io_Poll_Watcher, remove) php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis()); if (!intern->active || !intern->context) { + /* Closing the stream already removed it, so this is just the expected cleanup */ + if (intern->closed) { + return; + } zend_throw_exception( php_io_poll_inactive_watcher_class_entry, "Cannot remove inactive watcher", 0); RETURN_THROWS(); } - php_io_poll_context_object *context = intern->context; - php_poll_ctx *poll_ctx = context->ctx; - HashTable *watchers = context->watchers; - zend_ulong hash_key = php_io_poll_compute_ptr_key(intern->handle); - php_socket_t fd = php_poll_handle_get_fd(intern->handle); - if (fd != SOCK_ERR) { - php_poll_remove(poll_ctx, (int) fd); - } - - intern->active = false; - intern->context = NULL; - - if (watchers) { - zend_hash_index_del(watchers, hash_key); - } + php_io_poll_context_retire_watcher(intern->context, intern); } PHP_METHOD(Io_Poll_Context, __construct) @@ -715,6 +778,12 @@ PHP_METHOD(Io_Poll_Context, add) php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(getThis()); php_poll_handle_object *handle = PHP_POLL_HANDLE_OBJ_FROM_ZV(handle_obj); + events = php_io_poll_event_enums_to_events(event_enums); + if (!events) { + zend_argument_type_error(2, "must be array of Event enums"); + RETURN_THROWS(); + } + /* Get file descriptor */ php_socket_t fd = php_poll_handle_get_fd(handle); if (fd == SOCK_ERR) { @@ -723,16 +792,21 @@ PHP_METHOD(Io_Poll_Context, add) RETURN_THROWS(); } + zval *existing_zv = zend_hash_index_find(intern->watchers, (zend_ulong) fd); + if (existing_zv) { + php_io_poll_watcher_object *existing = PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(Z_OBJ_P(existing_zv)); + if (php_poll_handle_get_fd(existing->handle) == fd) { + zend_throw_exception( + php_io_poll_handle_already_watched_class_entry, "Handle already added", 0); + RETURN_THROWS(); + } + php_io_poll_context_retire_watcher(intern, existing); + } + /* Create watcher object */ object_init_ex(return_value, php_io_poll_watcher_class_entry); php_io_poll_watcher_object *watcher = PHP_POLL_WATCHER_OBJ_FROM_ZV(return_value); - events = php_io_poll_event_enums_to_events(event_enums); - if (!events) { - zend_argument_type_error(2, "must be array of Event enums"); - RETURN_THROWS(); - } - watcher->handle = handle; watcher->watched_events = events; watcher->triggered_events = 0; @@ -758,16 +832,22 @@ PHP_METHOD(Io_Poll_Context, add) RETURN_THROWS(); } - /* Store in our watchers map using shifted pointer as key */ + /* Store in our watchers map */ zval watcher_zv; ZVAL_OBJ(&watcher_zv, &watcher->std); GC_ADDREF(&watcher->std); - - zend_ulong hash_key = php_io_poll_compute_ptr_key(handle); - zend_hash_index_add_new(intern->watchers, hash_key, &watcher_zv); + zend_hash_index_add_new(intern->watchers, (zend_ulong) fd, &watcher_zv); watcher->active = true; watcher->context = intern; + watcher->fd = fd; + + if (handle->ops == &php_stream_poll_handle_ops) { + php_stream *stream = php_stream_poll_handle_get_stream(handle); + if (stream) { + php_io_poll_stream_watch(stream, watcher); + } + } } PHP_METHOD(Io_Poll_Context, wait) diff --git a/ext/standard/io_poll.h b/ext/standard/io_poll.h new file mode 100644 index 000000000000..1b94355f27a5 --- /dev/null +++ b/ext/standard/io_poll.h @@ -0,0 +1,26 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_POLL_H +#define PHP_IO_POLL_H + +#include "php_streams.h" + +BEGIN_EXTERN_C() + +PHPAPI void php_io_poll_stream_notify_close(php_stream *stream); + +END_EXTERN_C() + +#endif /* PHP_IO_POLL_H */ diff --git a/ext/standard/tests/poll/poll_stream_closed_dup_fd.phpt b/ext/standard/tests/poll/poll_stream_closed_dup_fd.phpt new file mode 100644 index 000000000000..9fafd91a46aa --- /dev/null +++ b/ext/standard/tests/poll/poll_stream_closed_dup_fd.phpt @@ -0,0 +1,49 @@ +--TEST-- +Io\Poll: a watched stream closed while a duplicated fd exists is unregistered in time +--SKIPIF-- +isAvailable()) { + die("skip Epoll backend required\n"); +} +?> +--FILE-- +add(new StreamPollHandle($r), [Io\Poll\Event::Read]); + +$proc = proc_open('sleep 2', [0 => $r], $pipes); +fclose($r); +$watcher->remove(); +unset($watcher); +gc_collect_cycles(); + +fwrite($w, "ping"); +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; + +// A new stream that takes the same fd number gets its own registration +list($r2, $w2) = pt_new_socket_pair(); +$watcher2 = $ctx->add(new StreamPollHandle($r2), [Io\Poll\Event::Read], "new"); +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; +fwrite($w2, "pong"); +$events = $ctx->wait(Time\Duration::fromMicroseconds(100000)); +echo "Events count: ", count($events), "\n"; +var_dump($events[0]->getData()); + +proc_terminate($proc); +proc_close($proc); +echo "done\n"; +?> +--EXPECT-- +Events count: 0 +Events count: 0 +Events count: 1 +string(3) "new" +done diff --git a/ext/standard/tests/poll/poll_stream_closed_fd_reuse.phpt b/ext/standard/tests/poll/poll_stream_closed_fd_reuse.phpt new file mode 100644 index 000000000000..74c359f301bf --- /dev/null +++ b/ext/standard/tests/poll/poll_stream_closed_fd_reuse.phpt @@ -0,0 +1,47 @@ +--TEST-- +Io\Poll: a new stream reusing the fd number of a closed watched stream can be added +--SKIPIF-- + +--FILE-- +add(new StreamPollHandle($r0), [Io\Poll\Event::Read], "old"); + +// The lowest free fd number is handed out first, so $r1 takes the number $r0 had +fclose($r0); +list($r1, $w1) = pt_new_socket_pair(); + +$new = $ctx->add(new StreamPollHandle($r1), [Io\Poll\Event::Read], "new"); +var_dump($old->isActive()); +var_dump($new->isActive()); + +fwrite($w1, "ping"); +$events = $ctx->wait(Time\Duration::fromMicroseconds(100000)); +var_dump(count($events)); +var_dump($events[0] === $new); +var_dump($events[0]->getData()); + +$old->remove(); +echo "old removed\n"; + +$new->remove(); +var_dump($new->isActive()); +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; +?> +--EXPECT-- +bool(false) +bool(true) +int(1) +bool(true) +string(3) "new" +old removed +bool(false) +Events count: 0 diff --git a/ext/standard/tests/poll/poll_stream_closed_fd_reuse_unwatched.phpt b/ext/standard/tests/poll/poll_stream_closed_fd_reuse_unwatched.phpt new file mode 100644 index 000000000000..bf699c948586 --- /dev/null +++ b/ext/standard/tests/poll/poll_stream_closed_fd_reuse_unwatched.phpt @@ -0,0 +1,36 @@ +--TEST-- +Io\Poll: an unwatched stream reusing the fd number of a closed watched stream is never reported +--SKIPIF-- + +--FILE-- +add(new StreamPollHandle($r0), [Io\Poll\Event::Read], "old"); + +// The lowest free fd number is handed out first, so $r1 takes the number $r0 had +fclose($r0); +list($r1, $w1) = pt_new_socket_pair(); +fwrite($w1, "ping"); + +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; +var_dump($old->isActive()); + +$old->remove(); +var_dump($old->isActive()); +var_dump(fread($r1, 10)); +?> +--EXPECT-- +Events count: 0 +Events count: 0 +bool(false) +bool(false) +string(4) "ping" diff --git a/ext/standard/tests/poll/poll_stream_closed_many.phpt b/ext/standard/tests/poll/poll_stream_closed_many.phpt new file mode 100644 index 000000000000..3f436fc940a6 --- /dev/null +++ b/ext/standard/tests/poll/poll_stream_closed_many.phpt @@ -0,0 +1,52 @@ +--TEST-- +Io\Poll: many watchers closed before removal leave the context consistent +--FILE-- +add(new StreamPollHandle($rk), [Io\Poll\Event::Read], "keep"); + +list($ro, $wo) = pt_new_socket_pair(); +$oneshot = $ctx->add(new StreamPollHandle($ro), [Io\Poll\Event::Read, Io\Poll\Event::OneShot], "oneshot"); +fwrite($wo, "x"); +$events = $ctx->wait(Time\Duration::fromMicroseconds(100000)); +var_dump(count($events), $events[0]->getData()); + +for ($i = 0; $i < 200; $i++) { + list($r, $w) = pt_new_socket_pair(); + $watcher = $ctx->add(new StreamPollHandle($r), [Io\Poll\Event::Read]); + fclose($r); + $watcher->remove(); + fclose($w); +} + +// The fired one-shot watcher stays disarmed, the kept one is still watched +fwrite($wk, "y"); +$events = $ctx->wait(Time\Duration::fromMicroseconds(100000)); +var_dump(count($events), $events[0]->getData()); +var_dump($keep->isActive(), $oneshot->isActive()); + +$oneshot->modifyEvents([Io\Poll\Event::Read, Io\Poll\Event::OneShot]); +$events = $ctx->wait(Time\Duration::fromMicroseconds(100000)); +var_dump(count($events)); + +list($r, $w) = pt_new_socket_pair(); +$new = $ctx->add(new StreamPollHandle($r), [Io\Poll\Event::Read], "new"); +fwrite($w, "z"); +$events = $ctx->wait(Time\Duration::fromMicroseconds(100000)); +var_dump(count($events)); +var_dump($oneshot->isActive()); +?> +--EXPECT-- +int(1) +string(7) "oneshot" +int(1) +string(4) "keep" +bool(true) +bool(true) +int(2) +int(2) +bool(true) diff --git a/ext/standard/tests/poll/poll_stream_closed_multi_context.phpt b/ext/standard/tests/poll/poll_stream_closed_multi_context.phpt new file mode 100644 index 000000000000..c14b4803bee2 --- /dev/null +++ b/ext/standard/tests/poll/poll_stream_closed_multi_context.phpt @@ -0,0 +1,34 @@ +--TEST-- +Io\Poll: closing a stream watched in several contexts leaves every watcher removable +--FILE-- +add($handle, [Io\Poll\Event::Read]); +$w2 = $ctx2->add($handle, [Io\Poll\Event::Read]); + +fwrite($w, "ping"); +fclose($r); + +echo "ctx1 events count: ", count($ctx1->wait(Time\Duration::fromSeconds(0))), "\n"; +echo "ctx2 events count: ", count($ctx2->wait(Time\Duration::fromSeconds(0))), "\n"; +var_dump($w1->isActive()); +var_dump($w2->isActive()); + +$w1->remove(); +$w2->remove(); +echo "removed\n"; + +fclose($w); +?> +--EXPECT-- +ctx1 events count: 0 +ctx2 events count: 0 +bool(false) +bool(false) +removed diff --git a/ext/standard/tests/poll/poll_stream_closed_persistent.phpt b/ext/standard/tests/poll/poll_stream_closed_persistent.phpt new file mode 100644 index 000000000000..b26f24b7cc9a --- /dev/null +++ b/ext/standard/tests/poll/poll_stream_closed_persistent.phpt @@ -0,0 +1,28 @@ +--TEST-- +Io\Poll: a watcher on a persistent stream is unregistered at request shutdown +--SKIPIF-- + +--FILE-- +add(new StreamPollHandle($p), [Io\Poll\Event::Write]); +echo "Events count: ", count($ctx->wait(Time\Duration::fromMicroseconds(100000))), "\n"; + +// The persistent stream outlives the request, so the watcher and context are +// intentionally left to be freed at shutdown +echo "done\n"; +?> +--EXPECT-- +Events count: 1 +done diff --git a/ext/standard/tests/poll/poll_stream_closed_watcher_inactive.phpt b/ext/standard/tests/poll/poll_stream_closed_watcher_inactive.phpt new file mode 100644 index 000000000000..7c5e943ebe5a --- /dev/null +++ b/ext/standard/tests/poll/poll_stream_closed_watcher_inactive.phpt @@ -0,0 +1,38 @@ +--TEST-- +Io\Poll: closing a watched stream deactivates its watcher and remove() stays harmless +--FILE-- +add(new StreamPollHandle($r), [Io\Poll\Event::Read]); + +fwrite($w, "ping"); +var_dump($watcher->isActive()); + +fclose($r); +var_dump($watcher->isActive()); + +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; + +$watcher->remove(); +$watcher->remove(); +echo "removed\n"; +var_dump($watcher->isActive()); + +try { + $watcher->modifyEvents([Io\Poll\Event::Write]); +} catch (Io\Poll\InactiveWatcherException $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} + +fclose($w); +?> +--EXPECT-- +bool(true) +bool(false) +Events count: 0 +removed +bool(false) +Io\Poll\InactiveWatcherException: Cannot modify inactive watcher diff --git a/ext/standard/tests/poll/poll_stream_handle_closed_stream.phpt b/ext/standard/tests/poll/poll_stream_handle_closed_stream.phpt index 56b5c08fadef..5e9e6729aa98 100644 --- a/ext/standard/tests/poll/poll_stream_handle_closed_stream.phpt +++ b/ext/standard/tests/poll/poll_stream_handle_closed_stream.phpt @@ -19,7 +19,7 @@ var_dump(get_debug_type($handle->getStream())); try { $watcher->modifyEvents([Io\Poll\Event::Write]); -} catch (Io\Poll\InvalidHandleException $e) { +} catch (Io\Poll\InactiveWatcherException $e) { echo $e->getMessage(), "\n"; } @@ -37,7 +37,7 @@ var_dump($watcher->isActive()); --EXPECT-- bool(false) string(17) "resource (closed)" -Invalid handle for polling +Cannot modify inactive watcher Invalid handle for polling Events count: 0 bool(false) diff --git a/ext/standard/tests/poll/poll_stream_oneshot_rearm.phpt b/ext/standard/tests/poll/poll_stream_oneshot_rearm.phpt new file mode 100644 index 000000000000..5ccc43113dae --- /dev/null +++ b/ext/standard/tests/poll/poll_stream_oneshot_rearm.phpt @@ -0,0 +1,44 @@ +--TEST-- +Io\Poll: a fired one-shot watcher stays active and is re-armed by modifyEvents() +--FILE-- +add($handle, [Io\Poll\Event::Read, Io\Poll\Event::OneShot], "data"); + +fwrite($w, "a"); +echo "Events count: ", count($ctx->wait(Time\Duration::fromMicroseconds(100000))), "\n"; +var_dump(fread($r, 10)); + +fwrite($w, "b"); +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; +var_dump($watcher->isActive()); + +try { + $ctx->add($handle, [Io\Poll\Event::Read]); +} catch (Io\Poll\HandleAlreadyWatchedException $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} + +$watcher->modifyEvents([Io\Poll\Event::Read, Io\Poll\Event::OneShot]); +$events = $ctx->wait(Time\Duration::fromMicroseconds(100000)); +echo "Events count: ", count($events), "\n"; +var_dump($events[0] === $watcher); +var_dump(fread($r, 10)); + +$watcher->remove(); +var_dump($watcher->isActive()); +?> +--EXPECT-- +Events count: 1 +string(1) "a" +Events count: 0 +bool(true) +Io\Poll\HandleAlreadyWatchedException: Handle already added +Events count: 1 +bool(true) +string(1) "b" +bool(false) diff --git a/main/php_streams.h b/main/php_streams.h index fb0c57ecf83d..69bd996fea4f 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -250,6 +250,8 @@ struct _php_stream { struct _php_stream *enclosing_stream; /* this is a private stream owned by enclosing_stream */ zend_llist *error_list; + + HashTable *poll_watchers; /* Io\Poll watchers notified before the stream is closed */ }; /* php_stream */ #define PHP_STREAM_CONTEXT(stream) \ diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index 19ff0ad6d228..f2c0212f4d15 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -279,6 +279,11 @@ static zend_result kqueue_backend_remove(php_poll_ctx *ctx, int fd) return FAILURE; } + /* Remove from tracking */ + if (!ctx->raw_events) { + zend_hash_index_del(backend_data->fd_tracking, fd); + } + /* If no filters were successfully deleted, that's an error */ if (successful_deletes == 0) { php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); @@ -289,11 +294,6 @@ static zend_result kqueue_backend_remove(php_poll_ctx *ctx, int fd) backend_data->fd_count--; backend_data->filter_count -= successful_deletes; - /* Remove from tracking */ - if (!ctx->raw_events) { - zend_hash_index_del(backend_data->fd_tracking, fd); - } - return SUCCESS; } diff --git a/main/streams/streams.c b/main/streams/streams.c index 7cd63f0038d3..0862dc7081e5 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -28,6 +28,7 @@ #include "ext/standard/basic_functions.h" /* for BG(CurrentStatFile) */ #include "ext/standard/php_string.h" /* for php_memnstr, used by php_stream_get_record() */ #include "ext/uri/php_uri.h" +#include "ext/standard/io_poll.h" #include #include #include "php_streams_int.h" @@ -371,6 +372,11 @@ fprintf(stderr, "stream_free: %s:%p[%s] preserve_handle=%d release_cast=%d remov return ret; } + /* Watchers must unregister while the fd is still open */ + if (stream->poll_watchers) { + php_io_poll_stream_notify_close(stream); + } + ret = stream->ops->close(stream, preserve_handle ? 0 : 1); if (!ret) { ret = flush_result; @@ -386,6 +392,10 @@ fprintf(stderr, "stream_free: %s:%p[%s] preserve_handle=%d release_cast=%d remov } if (close_options & PHP_STREAM_FREE_RELEASE_STREAM) { + if (stream->poll_watchers) { + php_io_poll_stream_notify_close(stream); + } + while (stream->readfilters.head) { if (stream->readfilters.head->res != NULL) { zend_list_close(stream->readfilters.head->res);