Skip to content

Fix GH-25086: Io\Poll kqueue backend leaks memory on every modifyEvents() - #25087

Merged
bukka merged 1 commit into
php:PHP-8.6from
lazerg:fix/gh-25086-kqueue-modify-leak
Nov 1, 2026
Merged

bukka merged 1 commit into
php:PHP-8.6from
lazerg:fix/gh-25086-kqueue-modify-leak

Conversation

@lazerg

@lazerg lazerg commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

kqueue_backend_modify() only deleted the filters that weren't in the new event set, so when no delete succeeded it assumed the fd had no filters before and added the full new set to filter_count. Going from Read to Read+Write therefore counted the already registered read filter again. get_suitable_max_events() returns filter_count, so every wait() grew the struct kevent buffer by one entry per modify. It now deletes both filters first, which gives the real number of registered filters, and then adds the requested ones. As a side effect, switching an existing watcher to edge-triggered or one-shot now takes effect, since EV_ADD on an existing filter kept its old EV_CLEAR/EV_ONESHOT flags.

Re-arming a fired one-shot watcher grew the buffer the same way, because wait() never took the filters the kernel dropped out of filter_count. It now does, including the leftover filter it deletes for read+write one-shot watchers.

Fixes #25086

…vents()

kqueue_backend_modify() only deleted the filters that were not in the new
event set, so when no delete succeeded it assumed the FD had no filters
before and added the full new set to filter_count. Going from Read to
Read+Write therefore counted the already registered read filter again.
get_suitable_max_events() returns filter_count, so every wait() grew the
struct kevent buffer by one entry per modify. It now deletes both filters
first, which gives the real number of registered filters, and then adds
the requested ones. As a side effect, switching an existing watcher to
one-shot or edge-triggered now takes effect, since EV_ADD on an existing
filter kept its old EV_ONESHOT/EV_CLEAR flags.

Re-arming a fired one-shot watcher grew the buffer the same way, because
wait() never took the filters the kernel dropped out of filter_count. It
now does, including the leftover filter it deletes for read+write
one-shot watchers.

Closes phpGH-25087.

Co-authored-by: Jakub Zelenka <bukka@php.net>
@bukka
bukka force-pushed the fix/gh-25086-kqueue-modify-leak branch from 03f8d63 to c0b8710 Compare November 1, 2026 20:08
@bukka

bukka commented Nov 1, 2026

Copy link
Copy Markdown
Member

It makes sense. I did a bit of tidying and added additional test. It would be possible to make it cleaner and reduce number of syscalls but it would get much bigger so this is safer for 8.6. Will merge once CI green.

@bukka
bukka merged commit 1e1eb7f into php:PHP-8.6 Nov 1, 2026
18 checks passed
bukka added a commit that referenced this pull request Nov 1, 2026
* PHP-8.6:
  Fix GH-25086: Io\Poll kqueue backend leaks memory on every modifyEvents() (#25087)
arnaud-lb added a commit to marc-mabe/php-src that referenced this pull request Nov 2, 2026
* PHP-8.6: (158 commits)
  Fix ZEND_LONG_UINT_OVFL() accepting negative values on 32-bit platforms (php#24778)
  Don't use static closure cache when called_scope diverges
  ext/com_dotnet: Fix phpGH-25008 event source typelib crash
  Fix phpGH-25093: Phar::webPhar() redirect reads past the request URI (php#25102)
  Fix the WSAPoll backend of Io\Poll accepting handles that are not sockets
  Fix phpGH-25086: Io\Poll kqueue backend leaks memory on every modifyEvents() (php#25087)
  openssl: Fix heap buffer overflow in TLS PSK callbacks
  openssl: Fix use-after-free in TLS session remove callback
  [`%pS` migration] Use `%pS` in ext/standard/var.c
  [`%pS` migration] Use `%pS` in ext/standard/var_unserializer.re
  [`%pS` migration] Use `%pS` in ext/standard/string.c
  [`%pS` migration] Use `%pS` in ext/standard/streamsfuncs.c
  [`%pS` migration] Use `%pS` in ext/standard/proc_open.c
  [`%pS` migration] Use `%pS` in ext/standard/mail.c
  [`%pS` migration] Use `%pS` in ext/standard/info.c
  [`%pS` migration] Use `%pS` in ext/standard/http.c
  [`%pS` migration] Use `%pS` in ext/standard/http_fopen_wrapper.c
  [`%pS` migration] Use `%pS` in ext/standard/head.c
  [`%pS` migration] Use `%pS` in ext/standard/ftp_fopen_wrapper.c
  [`%pS` migration] Use `%pS` in ext/standard/fsock.c
  ...
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.

2 participants