Skip to content

picoev: survive peer RST — ignore SIGPIPE at loop creation + SO_NOSIGPIPE on accepted sockets#27697

Open
eptx wants to merge 1 commit into
vlang:masterfrom
cx-home:upstream/picoev-sigpipe
Open

picoev: survive peer RST — ignore SIGPIPE at loop creation + SO_NOSIGPIPE on accepted sockets#27697
eptx wants to merge 1 commit into
vlang:masterfrom
cx-home:upstream/picoev-sigpipe

Conversation

@eptx

@eptx eptx commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

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 report EPIPE and the caller drops the fd.
  • setup_sock(): SO_NOSIGPIPE on macOS/FreeBSD — accepted sockets do not inherit the option from the listener on the BSDs (Linux has no SO_NOSIGPIPE; the signal_ignore branch 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.

…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.
@Jengro777

Copy link
Copy Markdown
Contributor

Is it really necessary to keep the picoev library in the standard library?

@medvednikov

Copy link
Copy Markdown
Member

Yes, it's fine in vlib.

@medvednikov

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread vlib/picoev/picoev.v
// 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants