Fundamentals 15 min read

Understanding SIGPIPE: Causes, Kernel Handling, and Mitigation in Go‑Rust Services

This article explains how the SIGPIPE signal is generated in Linux, why it can cause Go‑Rust services to crash without a core dump, and how to prevent it by configuring the signal handler to ignore SIGPIPE.

Refining Core Development Skills
Refining Core Development Skills
Refining Core Development Skills
Understanding SIGPIPE: Causes, Kernel Handling, and Mitigation in Go‑Rust Services

Fault Background

During a recent gray‑release, a core Go service that had been partially rewritten in Rust began crashing whenever a dependent service was hot‑upgraded. The crash occurred without leaving a core dump, and the only clue was a SIGPIPE signal triggered by a broken TCP connection.

Initial investigation showed no useful logs and no core files, so the team examined the coredump (which was absent) and eventually traced the issue to the SIGPIPE signal, which was resolved by setting the process to ignore SIGPIPE (SIGIGN).

1. How SIGPIPE Occurs

When a TCP connection is broken (e.g., due to network failure or peer restart), the kernel still allows user‑space programs to call write or send . During the send path, the kernel detects the broken socket and sends a SIGPIPE signal to the calling process.

The relevant kernel code path is do_tcp_sendpages which, upon detecting an error, calls sk_stream_error . The sk_stream_error function issues send_sig(SIGPIPE, current, 0) when the error is -EPIPE and the MSG_NOSIGNAL flag is not set.

ssize_t do_tcp_sendpages(struct sock *sk, struct page *page, int offset,
    size_t size, int flags) {
    ...
    err = -EPIPE;
    if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
        goto out_err;
out_err:
    return sk_stream_error(sk, flags, err);
}
int sk_stream_error(struct sock *sk, int flags, int err) {
    ...
    if (err == -EPIPE && !(flags & MSG_NOSIGNAL))
        send_sig(SIGPIPE, current, 0);
    return err;
}

2. Kernel SIGPIPE Handling Process

When the process returns from kernel mode to user mode, the kernel checks for pending signals. If a SIGPIPE is pending, the signal handling flow goes through do_notify_resume to do_signal , which retrieves the signal via get_signal and then dispatches it.

static void do_signal(struct pt_regs *regs) {
    struct ksignal ksig;
    ...
    if (get_signal(&ksig)) {
        /* Deliver the signal */
        handle_signal(&ksig, regs);
        return;
    }
    ...
}

The get_signal function performs three main steps: (1) dequeue a pending signal, (2) check whether the user process has installed a handler (SIG_IGN, custom handler, or default), and (3) apply the kernel’s default behavior if no user handler is present.

bool get_signal(struct ksignal *ksig) {
    for (;;) {
        /* 1. Dequeue signal */
        signr = dequeue_synchronous_signal(&ksig->info);
        if (!signr)
            signr = dequeue_signal(current, &current->blocked,
                                   &ksig->info, &type);
        /* 2. Check user‑defined handler */
        if (ka->sa.sa_handler == SIG_IGN)
            continue;
        if (ka->sa.sa_handler != SIG_DFL) {
            ksig->ka = *ka;
            break;
        }
        /* 3. Kernel default handling */
        ...
    }
    ksig->sig = signr;
    return ksig->sig > 0;
}

The default handling is divided into four categories:

Signals that are ignored by default (e.g., SIGCONT, SIGCHLD, SIGWINCH, SIGURG).

Signals that stop the process (e.g., SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU).

Signals that generate a core dump and terminate the process (e.g., SIGSEGV, SIGABRT, SIGFPE).

All other signals, including SIGPIPE, invoke do_group_exit , which terminates the process group without producing a core dump.

/* Default for SIGPIPE and similar */
if (sig_kernel_coredump(signr)) {
    do_coredump(&ksig->info);
} else {
    do_group_exit(ksig->info.si_signo);
}

3. Application‑Level Mitigation

Because the service did not install a SIGPIPE handler, the kernel’s default action (group exit without core dump) caused the crash. The fix is to explicitly ignore SIGPIPE in the Rust component that is called via cgo.

// Set SIGPIPE handler to ignore in Rust
let ignore_action = SigAction::new(
    SigHandler::SigIgn, // ignore signal
    signal::SaFlags::empty(),
    SigSet::empty(),
);
unsafe {
    signal::sigaction(Signal::SIGPIPE, &ignore_action)
        .expect("Failed to set SIGPIPE handler to ignore");
}

With this handler in place, the kernel’s get_signal sees SIG_IGN and simply skips the signal, preventing the process from being terminated.

Go’s runtime already ignores SIGPIPE on non‑standard file descriptors, but when Go calls into Rust via cgo, the signal may be delivered on a non‑Go thread, bypassing Go’s handling. Therefore, configuring the Rust side to ignore SIGPIPE is essential in mixed Go‑Rust deployments.

Summary

The crash was caused by a hot‑upgrade that broke a TCP connection, leading the kernel to send SIGPIPE, which the Go‑Rust service did not handle. The kernel’s default action terminated the process without a core dump. By installing an ignore handler for SIGPIPE in the Rust code, the issue was fully resolved.

rustKernelGoLinuxSignal HandlingcgoSIGPIPE
Refining Core Development Skills
Written by

Refining Core Development Skills

Fei has over 10 years of development experience at Tencent and Sogou. Through this account, he shares his deep insights on performance.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.