Commit 6fe34822f88 for php.net
commit 6fe34822f88528a1bad8dd6b874cc1d699c20517
Author: Weilin Du <weilindu@php.net>
Date: Mon Jul 20 22:25:46 2026 +0800
Fix GH-22759: Keep Io\Poll watcher registry in sync (#22760)
Fixed #22759 by removing the watcher from the Context registry when it is
explicitly removed, and rolling back the backend registration on failure.
diff --git a/ext/standard/io_poll.c b/ext/standard/io_poll.c
index 837470a9f75..ca363c7b37d 100644
--- a/ext/standard/io_poll.c
+++ b/ext/standard/io_poll.c
@@ -44,6 +44,8 @@ static zend_object_handlers php_io_poll_context_object_handlers;
static zend_object_handlers php_io_poll_watcher_object_handlers;
static zend_object_handlers php_io_poll_handle_object_handlers;
+typedef struct php_io_poll_context_object php_io_poll_context_object;
+
/* Watcher object structure */
typedef struct php_io_poll_watcher_object {
php_poll_handle_object *handle;
@@ -51,16 +53,16 @@ typedef struct php_io_poll_watcher_object {
uint32_t triggered_events;
zval data;
bool active;
- php_poll_ctx *poll_ctx; /* Back reference to poll context */
+ php_io_poll_context_object *context; /* Back reference to Context object */
zend_object std;
} php_io_poll_watcher_object;
/* Context object structure */
-typedef struct php_io_poll_context_object {
+struct php_io_poll_context_object {
php_poll_ctx *ctx;
HashTable *watchers; /* Maps handle pointer -> watcher object */
zend_object std;
-} php_io_poll_context_object;
+};
/* Stream poll handle specific data */
typedef struct php_stream_poll_handle_data {
@@ -251,7 +253,7 @@ 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->poll_ctx = NULL;
+ intern->context = NULL;
ZVAL_NULL(&intern->data);
return &intern->std;
@@ -293,7 +295,7 @@ static void php_io_poll_context_free_object(zend_object *obj)
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->poll_ctx = NULL;
+ watcher->context = NULL;
} ZEND_HASH_FOREACH_END();
}
@@ -349,7 +351,7 @@ static zend_always_inline zend_ulong php_io_poll_compute_ptr_key(void *ptr)
static zend_result php_io_poll_watcher_modify_events(
php_io_poll_watcher_object *watcher, uint32_t events)
{
- if (!watcher->active || !watcher->poll_ctx) {
+ if (!watcher->active || !watcher->context) {
zend_throw_exception(
php_io_poll_inactive_watcher_class_entry, "Cannot modify inactive watcher", 0);
return FAILURE;
@@ -363,8 +365,9 @@ static zend_result php_io_poll_watcher_modify_events(
}
/* Modify in poll context */
- if (php_poll_modify(watcher->poll_ctx, (int) fd, events, watcher) != SUCCESS) {
- php_poll_error err = php_poll_get_error(watcher->poll_ctx);
+ php_poll_ctx *poll_ctx = watcher->context->ctx;
+ if (php_poll_modify(poll_ctx, (int) 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);
return FAILURE;
@@ -632,19 +635,27 @@ PHP_METHOD(Io_Poll_Watcher, remove)
php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis());
- if (!intern->active || !intern->poll_ctx) {
+ if (!intern->active || !intern->context) {
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(intern->poll_ctx, (int) fd);
+ php_poll_remove(poll_ctx, (int) fd);
}
intern->active = false;
- intern->poll_ctx = NULL;
+ intern->context = NULL;
+
+ if (watchers) {
+ zend_hash_index_del(watchers, hash_key);
+ }
}
PHP_METHOD(Io_Poll_Context, __construct)
@@ -727,8 +738,6 @@ PHP_METHOD(Io_Poll_Context, add)
watcher->handle = handle;
watcher->watched_events = events;
watcher->triggered_events = 0;
- watcher->active = true;
- watcher->poll_ctx = intern->ctx;
GC_ADDREF(&handle->std);
@@ -757,7 +766,10 @@ PHP_METHOD(Io_Poll_Context, add)
GC_ADDREF(&watcher->std);
zend_ulong hash_key = php_io_poll_compute_ptr_key(handle);
- zend_hash_index_add(intern->watchers, hash_key, &watcher_zv);
+ zend_hash_index_add_new(intern->watchers, hash_key, &watcher_zv);
+
+ watcher->active = true;
+ watcher->context = intern;
}
PHP_METHOD(Io_Poll_Context, wait)
diff --git a/ext/standard/tests/poll/gh22759.phpt b/ext/standard/tests/poll/gh22759.phpt
new file mode 100644
index 00000000000..3df68c4f131
--- /dev/null
+++ b/ext/standard/tests/poll/gh22759.phpt
@@ -0,0 +1,38 @@
+--TEST--
+GH-22759 (Io\Poll: re-adding a removed handle keeps the new watcher tracked)
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+[$r, $w] = pt_new_socket_pair();
+$context = pt_new_stream_poll();
+$handle = new StreamPollHandle($r);
+
+$first = $context->add($handle, [Io\Poll\Event::Read]);
+$first->remove();
+
+$ref = WeakReference::create($first);
+unset($first);
+gc_collect_cycles();
+var_dump($ref->get());
+
+$second = $context->add($handle, [Io\Poll\Event::Read]);
+
+unset($context);
+gc_collect_cycles();
+
+var_dump($second->isActive());
+
+try {
+ $second->remove();
+} catch (Io\Poll\InactiveWatcherException $e) {
+ echo $e->getMessage(), "\n";
+}
+
+echo "done\n";
+?>
+--EXPECT--
+NULL
+bool(false)
+Cannot remove inactive watcher
+done