From 0df426f80ca3a1653073f0dd5bb57db7d0c58bc7 Mon Sep 17 00:00:00 2001 From: eptx Date: Tue, 7 Jul 2026 23:13:47 -0600 Subject: [PATCH] =?UTF-8?q?picoev:=20survive=20peer=20RST=20=E2=80=94=20ig?= =?UTF-8?q?nore=20SIGPIPE=20at=20loop=20creation=20+=20SO=5FNOSIGPIPE=20on?= =?UTF-8?q?=20sockets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- vlib/picoev/picoev.v | 10 ++++++++++ vlib/picoev/socket_util.c.v | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/vlib/picoev/picoev.v b/vlib/picoev/picoev.v index eb15913a4993b4..99bbd4049f5cd7 100644 --- a/vlib/picoev/picoev.v +++ b/vlib/picoev/picoev.v @@ -1,6 +1,7 @@ module picoev import net +import os import pico_http_parser import time @@ -307,6 +308,15 @@ fn default_error_callback(_data voidptr, _req pico_http_parser.Request, mut res // new creates a `Picoev` struct and initializes the main loop. pub fn new(config Config) !&Picoev { + // A server must not die on a peer RST: any write to a connection the client + // already reset delivers SIGPIPE, whose default action terminates the process + // SILENTLY (no panic, no crash report). Long-held fds (SSE) make this routine + // rather than rare — a subscriber that disconnects between pushes turns the + // 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) + } listening_socket_fd := listen(config) or { elog('Error during listen: ${err}') return err diff --git a/vlib/picoev/socket_util.c.v b/vlib/picoev/socket_util.c.v index 27f85d7b680124..e32d729bfe64bd 100644 --- a/vlib/picoev/socket_util.c.v +++ b/vlib/picoev/socket_util.c.v @@ -44,6 +44,16 @@ fn setup_sock(fd int) ! { if C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_NODELAY, &flag, sizeof(int)) < 0 { return error('setup_sock.setup_sock failed') } + $if freebsd || macos { + // Accepted sockets do not inherit SO_NOSIGPIPE from the listener on the + // BSDs; without it a write() after a peer RST raises SIGPIPE instead of + // returning EPIPE. Belt to the process-wide signal_ignore braces in new() + // — either alone suffices, together they survive callers that re-arm + // SIGPIPE handlers. + if C.setsockopt(fd, C.SOL_SOCKET, C.SO_NOSIGPIPE, &flag, sizeof(int)) < 0 { + return error('setup_sock: SO_NOSIGPIPE failed') + } + } $if freebsd { if C.fcntl(fd, C.F_SETFL, C.SOCK_NONBLOCK) != 0 { return error('fcntl failed')