[php-src] master: Fix GH-22759: Keep Io\Poll watcher registry in sync (#22760)

Weilin Du via GitHub <[email protected]> Mon, 20 Jul 2026 14:25:50 +0000
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Weilin Du (LamentXU123)
Committer: GitHub (web-flow)
Pusher: LamentXU123
Date: 2026-07-20T22:25:46+08:00

Commit: https://github.com/php/php-src/commit/6fe34822f88528a1bad8dd6b874cc1d699c20517
Raw diff: https://github.com/php/php-src/commit/6fe34822f88528a1bad8dd6b874cc1d699c20517.diff

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.

Changed paths:
  A  ext/standard/tests/poll/gh22759.phpt
  M  ext/standard/io_poll.c


Diff:

diff --git a/ext/standard/io_poll.c b/ext/standard/io_poll.c
index 837470a9f753..ca363c7b37d9 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 000000000000..3df68c4f1314
--- /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