From b44b9a9dc365630bacff3a2e03efe96fbbe66fa3 Mon Sep 17 00:00:00 2001 From: Albert Slepak Date: Wed, 12 Aug 2026 23:13:49 -0700 Subject: [PATCH] feat(signals): raised SIGPIPE for writes with no reader A write to a closed pipe or shut-down TCP stream only reported EPIPE, so naive pipeline writers looped on errors instead of dying the POSIX way. The failing write sites now raise SIGPIPE in the writer, and the existing disposition machinery kills, delivers, or drops it. Co-authored-by: Cursor --- kernel/net/tcp.cpp | 12 +++-- kernel/pipe/pipe.cpp | 8 ++++ userland/apps/sigtest/src/sigtest.c | 73 ++++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/kernel/net/tcp.cpp b/kernel/net/tcp.cpp index af0f4e49..d91c1e78 100644 --- a/kernel/net/tcp.cpp +++ b/kernel/net/tcp.cpp @@ -481,10 +481,16 @@ __PRIVILEGED_CODE static ssize_t tcp_write( sync::irq_state irq = sync::spin_lock_irqsave(sock->lock); if (sock->shut_wr) { sync::spin_unlock_irqrestore(sock->lock, irq); - return (count > remaining) - ? static_cast(count - remaining) - : resource::ERR_PIPE; + if (count > remaining) { + return static_cast(count - remaining); + } + + // POSIX: writing a shut-down stream raises SIGPIPE, and write + // has no MSG_NOSIGNAL to suppress it. + signals::send_to_task(sched::current(), signals::SIGPIPE); + return resource::ERR_PIPE; } + tcp_state cur = sock->state; if (cur != tcp_state::ESTABLISHED && cur != tcp_state::CLOSE_WAIT) { sync::spin_unlock_irqrestore(sock->lock, irq); diff --git a/kernel/pipe/pipe.cpp b/kernel/pipe/pipe.cpp index bc1848e6..343b7dd7 100644 --- a/kernel/pipe/pipe.cpp +++ b/kernel/pipe/pipe.cpp @@ -3,6 +3,8 @@ #include "common/ring_buffer.h" #include "fs/fstypes.h" #include "mm/heap.h" +#include "sched/sched.h" +#include "signals/signal.h" #include "sync/poll.h" #include "dynpriv/dynpriv.h" @@ -74,6 +76,12 @@ static ssize_t pipe_write( RUN_ELEVATED({ result = ring_buffer_write(ep->channel->rb, static_cast(ksrc), count, nonblock); + + // POSIX: a write with no reader raises SIGPIPE in the writer, + // EPIPE only surfaces when the signal is ignored or handled + if (result == RB_ERR_PIPE) { + signals::send_to_task(sched::current(), signals::SIGPIPE); + } }); return result; } diff --git a/userland/apps/sigtest/src/sigtest.c b/userland/apps/sigtest/src/sigtest.c index 70465335..4addd134 100644 --- a/userland/apps/sigtest/src/sigtest.c +++ b/userland/apps/sigtest/src/sigtest.c @@ -119,6 +119,18 @@ static void test_unblock_delivers(void) { check("unblock delivers immediately", usr1_count == 1); } +/* Child mode: a write with no reader must die by default SIGPIPE */ +static int pipe_victim_child(void) { + int fds[2]; + if (pipe(fds) != 0) { + return 1; + } + close(fds[0]); + char b = 'x'; + write(fds[1], &b, 1); + return 1; /* only reached if the signal never fired */ +} + static volatile sig_atomic_t eintr_handler_ran = 0; static void eintr_handler(int sig) { @@ -302,7 +314,65 @@ static void test_poll_eintr_despite_restart(void) { close(fds[1]); } -int main(void) { +static volatile sig_atomic_t sigpipe_count = 0; + +static void sigpipe_handler(int sig) { + (void)sig; + sigpipe_count++; +} + +static void test_sigpipe_dispositions(void) { + int fds[2]; + char b = 'x'; + + /* Ignored: the write fails with EPIPE and the process lives */ + signal(SIGPIPE, SIG_IGN); + if (pipe(fds) != 0) { + printf(" SKIP: pipe unavailable\n"); + return; + } + close(fds[0]); + ssize_t n = write(fds[1], &b, 1); + int saved_errno = errno; + check("ignored SIGPIPE write fails EPIPE", n == -1 && saved_errno == EPIPE); + close(fds[1]); + + /* Handled: the handler runs and EPIPE is still returned */ + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = sigpipe_handler; + sigaction(SIGPIPE, &sa, NULL); + if (pipe(fds) != 0) { + printf(" SKIP: pipe unavailable\n"); + return; + } + close(fds[0]); + n = write(fds[1], &b, 1); + saved_errno = errno; + check("handled SIGPIPE write fails EPIPE", n == -1 && saved_errno == EPIPE); + check("SIGPIPE handler ran", sigpipe_count == 1); + close(fds[1]); + signal(SIGPIPE, SIG_DFL); + + /* Default: a child writing with no reader dies by SIGPIPE */ + static const char* args[] = { "--pipe-victim", NULL }; + int h = proc_create("/bin/sigtest", args); + if (h < 0) { + printf(" SKIP: self exec unavailable\n"); + return; + } + proc_start(h); + int status = 0; + proc_wait(h, &status); + check("default SIGPIPE kills the writer", + STLX_WIFSIGNALED(status) && STLX_WTERMSIG(status) == SIGPIPE); +} + +int main(int argc, char** argv) { + if (argc >= 2 && strcmp(argv[1], "--pipe-victim") == 0) { + return pipe_victim_child(); + } + setvbuf(stdout, NULL, _IONBF, 0); printf("sigtest: running signal delivery tests\n"); @@ -313,6 +383,7 @@ int main(void) { test_read_eintr(); test_read_restart(); test_poll_eintr_despite_restart(); + test_sigpipe_dispositions(); printf("sigtest: %d passed, %d failed\n", passed, failed); return failed > 0 ? 1 : 0;