Skip to content

CLI aborts with SIGABRT and dumps core when stdout closes early (railway --help | head is enough) #1126

Description

@jburchel

Piping any Railway CLI command into a consumer that closes the pipe early — head, grep -q, jq ... | head — kills the CLI with SIGABRT and leaves a core dump. It is not specific to any subcommand; railway --help | head -20 reproduces it.

The shell hides this, because a pipeline reports the last stage's exit status. railway status | head -20 looks like a clean success while the CLI has actually crashed.

CLI version

  • 5.43.2 (also seen on 5.43.1)
  • Installed via npm i -g @railway/cli, so the x86_64-unknown-linux-musl static build
  • Arch Linux, kernel 7.1.8, x86_64

Steps to reproduce

$ railway --help | head -20 > /dev/null; echo "railway exit: ${PIPESTATUS[0]}"
railway exit: 134

134 is 128 + 6, i.e. SIGABRT. coredumpctl confirms a real dump each time:

$ coredumpctl info --no-pager | head
           PID: 1607287 (railway)
        Signal: 6 (ABRT) si_code: SI_TKILL
  Command Line: .../@railway/cli/bin/railway --help

Same for a normal command whose output exceeds the head count:

$ railway status | head -5 > /dev/null; echo "railway exit: ${PIPESTATUS[0]}"
railway exit: 134

Running the same commands with a consumer that reads to EOF is clean:

railway --help | sed -n '1,20p'   # exit 0
railway --help | cat              # exit 0
railway --help > file             # exit 0

What I expected

Exit quietly, the way ls | head and every other well-behaved CLI does. Worst case a non-zero status — not a fatal signal and a core dump.

Cause

Capturing stderr while stdout is a broken pipe shows it:

$ railway --help 2>panic.txt | head -1 >/dev/null; cat panic.txt
thread 'main' (1652344) panicked at /rustc/88d9e12ae178fab0fb5cc050a94da85685d449ea/library/std/src/io/stdio.rs:1166:9:
failed printing to stdout: Broken pipe (os error 32)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Rust's runtime sets SIGPIPE to SIG_IGN at startup, so writing to a closed pipe returns EPIPE instead of terminating the process. println! panics on that error. Because the release profile uses panic = "abort", the panic becomes abort() rather than an unwind — hence SIGABRT with si_code: SI_TKILL (a self-directed raise) and a core dump instead of exit code 101.

Suggested fix

The conventional one-liner for a CLI is to restore the default disposition early in main, before any output:

unsafe { libc::signal(libc::SIGPIPE, libc::SIG_DFL) };

The process then dies from SIGPIPE exactly like cat or ls does, with no panic and no core. Alternatives, if you'd rather not touch signal state: treat ErrorKind::BrokenPipe from stdout writes as a clean exit(0), or route help/status output through a writer that swallows it.

Worth noting that even with panic = "unwind" this would still print a Rust panic message and exit 101 on a routine | head, so the signal handling is the part that matters.

Impact

Beyond the noise, the crash is invisible from the shell, so scripts and AI coding agents that do railway status | head believe the command succeeded. On a desktop with systemd-coredump enabled, every one of these leaves a core dump and fires a crash notification — I accumulated 30+ dumps over two days before tracking down what was producing them.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions