Conversation
When a worker exits while one of its child processes still holds the worker's stdout/stderr pipe open, the drain in fpm_child_close() gets EAGAIN instead of EOF and leaves the fd open. fpm_child_close() then closed the fd but kept its number in the child struct, so the postponed free closed it (and removed its event) a second time. If a new worker had been given the same fd number in the meantime, its pipe was closed and every later write it made to stdio failed with EPIPE. Remove the event and reset the fd to -1 when closing it in fpm_child_close() so the postponed free cannot act on it again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, afopen('php://stderr')dup gets inherited by a backgroundexec(). In that case the drain infpm_child_close()getsEAGAINinstead of EOF, sofpm_stdio_child_said()leaves the fd open.fpm_child_close()then closes the fd but keeps the number inchild->fd_stdout/child->fd_stderr. One second later,fpm_postponed_child_free()(added in the GH-10461 fix) callsfpm_event_del()andclose()on that same number again:epoll: unable to remove fd N.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 thingfpm_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_looppath, which runs in the master.fpm_children_free()(thein_event_loop = 0path) also runs during cleanup in forked workers, where the epoll/kqueue instance is shared with the master. Callingfpm_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.phptusespm = static,pm.max_children = 1andpm.max_requests = 1. The first request writes to a dup of stderr and starts a backgroundsleep, 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.write to stderr failed.I ran the rest of
sapi/fpm/testswith and without the fix and got the same results.gh-11086-daemonized-logs-duplicatedandsocket-uds-too-long-filename-testfail in my root-in-container setup either way.🤖 Generated with Claude Code