HAVE_EPOLL_PWAIT2 only tells whether the libc exports the wrapper, which
glibc does since 2.35 regardless of the running kernel. A PHP built on a
kernel with epoll_pwait2 and run on one older than 5.11 gets ENOSYS from
every Context::wait() call, which makes the epoll backend and thus the
Auto backend unusable. The same happens under emulation layers that do
not implement the syscall.
Try epoll_pwait2 first and on ENOSYS or ENOTSUP switch the process to
epoll_wait with a millisecond timeout, retrying the current call so the
failure is never visible to the caller. The flag is process wide since
kernel support is the same for every thread, and it is atomic so the
first concurrent waits in a ZTS build do not race on it.
The epoll backend reported everything but EEXIST and ENOENT as ERR_SYSTEM, so
a caller could not tell that epoll refuses the handle from a real failure. The
kqueue backend already maps errno, this makes epoll do the same, with EPERM
reported as ERR_NOSUPPORT: that is what epoll_ctl() returns for files that do
not implement polling, such as regular files and /dev/null.
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.
php_poll_timespec_to_ms() cast seconds * 1000 to an int, so a wait() of more than
about 24 days wrapped around: five years came out as 48ms on the poll backend.
The value is capped now. epoll is only affected where it falls back to
epoll_wait(), since epoll_pwait2() takes the timespec as is.
Rather than going through a whole callability check via call_user_function() we can grab the known method pointer and call it directly.
This removes some allocations
Cap each kevent() request at the number of entries that can still be
delivered instead of requesting twice the buffer size. Events that are
not retrieved (including oneshot and edge-triggered ones) stay pending
in the kqueue and are delivered by the next wait, matching the epoll
behavior. Merged read+write events can under-fill the buffer, so top up
with zero timeout rounds, and base the suitable max events on the
filter count so a default sized wait retrieves everything in one call.
php_poll_wait() reported a stale error code when a backend's wait syscall
failed without recording one: epoll, poll and kqueue returned -1 without
setting it. Record it from errno in those backends so php_poll_get_error()
reflects the actual failure. eventport and wsapoll already did, the
latter from WSAGetLastError().
Closes GH-22326
Several memory-safety issues in the new Io\Poll API, found by review and
confirmed under valgrind:
- Watcher kept a raw pointer to its Context's php_poll_ctx with no
reference, so dropping the Context while holding a Watcher left
remove()/modify() dereferencing freed memory (use-after-free). The
Context now neutralizes its watchers (active=false, poll_ctx=NULL)
before it is destroyed, so those calls throw InactiveWatcherException.
- StreamPollHandle took a reference on the stream resource in the
constructor but never released it, leaking the descriptor for the
rest of the request. Store the zend_resource and release it in the
handle cleanup; the php_stream may already be freed by then (e.g.
the user closed it), so the cleanup must not dereference it.
- Watcher and Context had no get_gc handler, so reference cycles through
Watcher::$data were uncollectable. Add get_gc for both.
- Context, Watcher and StreamPollHandle were cloneable through the
default handler, which shallow-copied the backing php_poll_ctx and the
watcher map by pointer and double-freed them on destruction. Mark all
three uncloneable.
- Calling __construct() a second time on a Context or StreamPollHandle
replaced the backing context or handle data without releasing the
first, leaking it. Throw if the object is already constructed.
- The add(), modify(), remove() and wait() entry points accepted a NULL
ctx and forwarded it to php_poll_set_error(), which dereferenced it.
The userland layer already gates on an active context before reaching
the C API, so assert a non-NULL ctx in those entry points instead.
Closes GH-22316