[php-src] PR #24056: Fix GH-24046: FPM worker stdio pipe fd closed twice

From: Date: Thu, 01 Oct 2026 19:43:53 +0000
Subject: [php-src] PR #24056: Fix GH-24046: FPM worker stdio pipe fd closed twice
Groups: php.git-pulls 
Request: Send a blank email to git-pulls+get-39052@lists.php.net to get a copy of this message
Pull Request: https://github.com/php/php-src/pull/24056 Author: ipc-zpg Fixes #24046 ### Problem With catch_workers_output = yes, a worker can exit while one of its child processes still holds its stdout/stderr pipe open. For example, a fopen('php://stderr') dup gets inherited by a background exec(). In that case the drain in fpm_child_close() gets EAGAIN instead of EOF, so fpm_stdio_child_said() leaves the fd open. fpm_child_close() then closes the fd but keeps the number in child->fd_stdout / child->fd_stderr. One second later, fpm_postponed_child_free() (added in the GH-10461 fix) calls fpm_event_del() and close() on that same number again: - If the number hasn't been reused, fpm logs epoll: unable to remove fd N. - If a new worker's pipe was given the number in the meantime, which is common because the master forks the replacement straight away, the new worker's event is removed and its read end is closed. From then on, every stdio write in that worker fails with EPIPE, and nothing is logged. ### Fix When fpm_child_close() closes the fd, it now also removes the event and sets the fd to -1. This is the same thing fpm_stdio_child_said() already does when it hits EOF or an error, and it stops the postponed free from acting on the fd again. The event is only removed on the in_event_loop path, which runs in the master. fpm_children_free() (the in_event_loop = 0 path) also runs during cleanup in forked workers, where the epoll/kqueue instance is shared with the master. Calling fpm_event_del() there could remove the master's registrations, so that path still only closes the fd and resets it. ### Test sapi/fpm/tests/gh24046-stdio-pipe-double-close.phpt uses pm = static, pm.max_children = 1 and pm.max_requests = 1. The first request writes to a dup of stderr and starts a background sleep, then the worker exits. The test waits for the postponed free and sends a second request, which must still be able to write to stderr. - Without the fix: the second request returns write to stderr failed. - With the fix: it passes (8/8 runs, Debian bookworm, epoll). I ran the rest of sapi/fpm/tests with and without the fix and got the same results. gh-11086-daemonized-logs-duplicated and socket-uds-too-long-filename-test fail in my root-in-container setup either way. 🤖 Generated with [Claude Code](https://claude.com/claude-code)

« previous php.git-pulls (#39052) next »