Skip to content

Fix GH-22844: Io\Poll watchers after the watched stream is closed - #23822

Open
bukka wants to merge 1 commit into
php:masterfrom
bukka:poll_stream_close_notify
Open

bukka wants to merge 1 commit into
php:masterfrom
bukka:poll_stream_close_notify

Conversation

@bukka

@bukka bukka commented Sep 21, 2026

Copy link
Copy Markdown
Member

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.

I tried couple of approaches but ended up following the approach from GH-22848 by @iliaal. It differs a bit and fixes few more things but the main idea is from there.

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 phpGH-22848 by Ilia Alshanetsky.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant