Skip to content

SIGABRT: panic in wait-timeout's SIGCHLD handler kills wt when the self-pipe send() fails #3856

Description

@tomascamargo

Summary

Any wt command that waits on a child process via shell_exec::Cmd::delayed_stream aborts with SIGABRT if the SIGCHLD notification write fails. wt inherits this from wait-timeout 0.2.1, whose SIGCHLD handler panics on any errno other than WouldBlock — and because the handler is extern "C", the panic cannot unwind and goes straight to abort().

I hit this reliably by running wt switch --create inside the OpenAI Codex CLI's Linux sandbox, which denies the sendto(2) syscall. But the sandbox is only the trigger; the crash-instead-of-degrade behaviour is worktrunk's to absorb.

What happens

wt -C <repo> -y switch --create <branch> --base <base> --no-hooks --no-cd --format json
→ thread caused non-unwinding panic. aborting.
→ SIGABRT, coredump

Symbolized backtrace (Arch worktrunk 0.68.0-1, symbols via debuginfod):

wt::main
 → wt::commands::worktree::switch::handle_switch_command
 → SwitchPipeline::run
 → execute_switch
 → worktrunk::git::repository::Repository::run_command_delayed_stream
 → worktrunk::shell_exec::Cmd::delayed_stream
 → wait_timeout::ChildExt::wait_timeout          →  poll()   ← blocked on child `git`
<signal handler called>                                      ← SIGCHLD
 → wait_timeout::imp::sigchld_handler
 → core::panicking::panic_cannot_unwind
 → std::sys::pal::unix::abort_internal
 → abort()

Panic message recovered from the core dump:

bad error on write fd: Operation not permitted (os error 1)

Mechanism

wait-timeout builds a self-notification AF_UNIX socketpair and pokes it from the SIGCHLD handler with send(fd, buf, 1, MSG_NOSIGNAL). Its notify() tolerates exactly one error:

match state.write.write(&[1]) {
    Ok(..) => {}
    Err(e) if e.kind() == WouldBlock => {}
    Err(e) => panic!("bad error on write fd: {}", e),   // ← anything else aborts the process
}

sigchld_handler is extern "C", so that panic hits panic_cannot_unwind and terminates the process immediately. There is no way for wt to catch it and no useful diagnostic for the user — just SIGABRT.

Reproduction

Deterministic, in a throwaway repo:

mkdir repro && cd repro && git init -b main && git commit --allow-empty -m init
codex sandbox -- wt switch --create repro-test --base main --no-cd --no-hooks
# thread caused non-unwinding panic. aborting.

(codex sandbox runs the command under Codex's default workspace-write sandbox, which denies sendto. Confirmed independently: send() on an AF_UNIX socketpair returns EPERM there while write() on the same fd succeeds.)

I have two independent coredumps six days apart — different repositories, different branches, different terminal sessions — with byte-identical stacks, so this is not a race.

Expected

Failing to poke an internal wakeup socket should not kill the process. Ideally wt either reports a normal error or, better, doesn't have this failure mode at all.

Notes on a fix

This is a known problem in the crate: alexcrichton/wait-timeout#45 flags exactly this notify() panic as an async-signal-safety violation (panic! in a signal handler allocates and takes locks). It's open and currently classified as minor/theoretical; this report is a real-world instance of it. Options, roughly in order of preference:

  1. Replace wait-timeout in src/shell_exec.rs. Since delayed_stream already polls, waiting on the child directly (e.g. pidfd on Linux — cf. Add pidfd-based implementation alexcrichton/wait-timeout#29 — or a waitpid-with-timeout loop) removes the global SIGCHLD handler entirely.
  2. Vendor/patch notify() to ignore all write errors, not just WouldBlock. A missed wakeup is at worst a delayed timeout; it is never worth an abort().

src/commands/picker/pager.rs and src/commands/picker/prs.rs use wait_timeout too and presumably have the same exposure.

Environment

Filed by Claude Opus 5 via Claude Code.

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions