openai/codex · error

waitpid failed for bubblewrap child: {err}

Error message

waitpid failed for bubblewrap child: {err}

What it means

wait_for_bwrap_child (linux_run_main.rs:930) reaps the forked bwrap child in a loop that retries only EINTR; any other errno panics. For a direct child pid the realistic failure is ECHILD: the child was already reaped elsewhere — a wait-all SIGCHLD handler in the host, another thread calling waitpid(-1), SA_NOCLDWAIT auto-reaping, or a subreaper redirecting the child. The panic aborts the launcher after the sandboxed workload already ran.

Source

Thrown at codex-rs/linux-sandbox/src/linux_run_main.rs:941

                let err = std::io::Error::last_os_error();
                panic!("failed to reset bubblewrap signal handler for {signal}: {err}");
            }
        }
    }
}

fn wait_for_bwrap_child(pid: libc::pid_t) -> libc::c_int {
    loop {
        let mut status: libc::c_int = 0;
        let wait_res = unsafe { libc::waitpid(pid, &mut status as *mut libc::c_int, 0) };
        if wait_res >= 0 {
            return status;
        }
        let err = std::io::Error::last_os_error();
        if err.raw_os_error() == Some(libc::EINTR) {
            continue;
        }
        panic!("waitpid failed for bubblewrap child: {err}");
    }
}

fn register_synthetic_mount_targets(
    targets: &[crate::bwrap::SyntheticMountTarget],
) -> Vec<SyntheticMountTargetRegistration> {
    with_synthetic_mount_registry_lock(|| {
        targets
            .iter()
            .map(|target| {
                let marker_dir = synthetic_mount_marker_dir(target.path());
                fs::create_dir_all(&marker_dir).unwrap_or_else(|err| {
                    panic!(
                        "failed to create synthetic bubblewrap mount marker directory {}: {err}",
                        marker_dir.display()
                    )
                });
                let target = if target.preserves_pre_existing_path()

View on GitHub (pinned to 339751715c)

Solutions

  1. Remove or scope the global SIGCHLD reaper so each code path reaps only the pids it forked.
  2. Ensure SA_NOCLDWAIT is not set on SIGCHLD while the sandbox runs.
  3. Read the errno in the panic: ECHILD means double reap; EINVAL or anything else means capture a backtrace and report upstream.
  4. Prefer exec'ing the launcher as its own process instead of embedding it in a child-managing supervisor.

Example fix

// before: global reaper steals the sandbox child's status
unsafe { libc::signal(libc::SIGCHLD, reap_all as libc::sighandler_t); }

// after: no global reaper; only the forking code waits its own pids
// (delete reap_all entirely, or make it match a specific pid)
Defensive patterns

Strategy: validation

Validate before calling

fn sigchld_not_auto_reaping() -> bool {
    let mut current: libc::sigaction = unsafe { std::mem::zeroed() };
    unsafe { libc::sigaction(libc::SIGCHLD, std::ptr::null(), &mut current) };
    current.sa_flags & libc::SA_NOCLDWAIT == 0
}

Prevention

When it happens

Trigger: Hosting or wrapping the sandbox launcher in a process that globally reaps children while run_bwrap_in_child_with_synthetic_mount_cleanup or run_bwrap_in_child_capture_stderr calls waitpid on its own forked pid.

Common situations: Supervisors with wait(-1) loops; runtimes or monitoring agents installing SIGCHLD handlers; SA_NOCLDWAIT set for zombie suppression; test harnesses that fork and reap globally.

Related errors


AI-assisted analysis of openai/codex@339751715c (2026-08-25). Data as JSON: /api/errors/b3762467422d7fd9. Report an issue: GitHub.