picoev: survive peer RST — ignore SIGPIPE at loop creation + SO_NOSIGPIPE on accepted sockets#27697
picoev: survive peer RST — ignore SIGPIPE at loop creation + SO_NOSIGPIPE on accepted sockets#27697eptx wants to merge 1 commit into
Conversation
…PIPE on sockets A server writing to a connection the client already reset receives SIGPIPE, whose default action terminates the process silently (no panic, no crash report). Long-held fds (SSE / streaming responses) make this routine rather than rare: a subscriber that disconnects between pushes turns the next push into a process kill. Two independent guards (either suffices; together they survive callers that re-arm SIGPIPE handlers): - picoev.new() ignores SIGPIPE process-wide (a server event loop exists). - setup_sock() sets SO_NOSIGPIPE on freebsd/macos, where accepted sockets do not inherit it from the listener. After this, write()/send() on a reset connection reports EPIPE and the caller drops the fd. Repro (external, cx server on this engine): SSE subscriber churn (curl -N --max-time 3 in a loop) killed the process in ~5s; with SIGPIPE ignored (trap '' PIPE inherited through exec) the same load ran indefinitely.
|
Is it really necessary to keep the picoev library in the standard library? |
|
Yes, it's fine in vlib. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0df426f80c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // next push into a process kill. Ignore it process-wide once a server event | ||
| // loop exists; write()/send() then report EPIPE and the caller drops the fd. | ||
| $if !windows { | ||
| os.signal_ignore(.pipe) |
There was a problem hiding this comment.
Add a per-send guard for re-armed SIGPIPE
On Linux/OpenBSD builds, this signal handler is the only new guard because the SO_NOSIGPIPE branch is macOS/FreeBSD-only and picohttpparser.Response.end() still calls send(..., 0). If an app or dependency installs a SIGPIPE handler/default disposition after new() (the re-arm case described here), a later write to a reset peer can still terminate the process; use MSG_NOSIGNAL/an OpenBSD per-send path for those platforms instead of relying solely on this global handler.
Useful? React with 👍 / 👎.
A picoev server writing to a connection the client already reset receives SIGPIPE, whose default action terminates the process silently (no panic, no crash report). Long-held fds — SSE / streaming responses — make this routine rather than rare: a subscriber that disconnects between pushes turns the next push into a process kill.
Two complementary guards (either alone suffices; together they survive callers that re-arm SIGPIPE handlers):
picoev.new():os.signal_ignore(.pipe)on non-Windows — a server event loop opts the process out of SIGPIPE; writes then reportEPIPEand the caller drops the fd.setup_sock():SO_NOSIGPIPEon macOS/FreeBSD — accepted sockets do not inherit the option from the listener on the BSDs (Linux has noSO_NOSIGPIPE; thesignal_ignorebranch covers it).Field evidence (downstream server built on picoev, macOS): an SSE endpoint under connect/RST churn died silently in under 2 minutes; with these guards the same binary survived the same load indefinitely, reaping the dead fds via EPIPE. A/B was controlled with
trap '' PIPE(survives) vs default disposition (dies), pinning the kill to SIGPIPE delivery on the post-RST write.