xai-org/grok-build · error · io::Error

refusing to killpg the caller's own process group ({pid})

Error message

refusing to killpg the caller's own process group ({pid})

What it means

Even when the pid is otherwise valid, ProcessGroup::new refuses a pid equal to the caller's current process group (getpgrp), because killpg on your own group would kill the caller itself. This is a self-preservation guard, reported as InvalidInput.

Source

Thrown at crates/codegen/xai-tty-utils/src/lib.rs:632

    /// was never grouped, which would otherwise broadcast the kill.
    pub fn new(pid: u32) -> io::Result<Self> {
        if pid <= 1 {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                format!("refusing degenerate process-group id {pid} (0 = own group, 1 = init)"),
            ));
        }
        // killpg_unix casts `pid as i32`; values > i32::MAX wrap to negative,
        // and killpg with a negative pgid returns EINVAL on Linux/macOS. Reject
        // here so the invariant is safe-by-construction, not safe-by-OS-quirk.
        if pid > i32::MAX as u32 {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                format!("process-group id {pid} exceeds i32::MAX; cannot be used with killpg"),
            ));
        }
        if i64::from(pid) == i64::from(nix::unistd::getpgrp().as_raw()) {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                format!("refusing to killpg the caller's own process group ({pid})"),
            ));
        }
        Ok(Self(pid))
    }

    /// The validated raw process-group id.
    pub fn get(self) -> u32 {
        self.0
    }
}

/// Process-tree teardown handle.
///
/// - Unix: holds the validated group-leader id ([`ProcessGroupId`]); dispatches
///   to `killpg(pgid, signal)`.
/// - Windows: holds a Job Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`.

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Only enroll pids of children spawned into a dedicated process group (verify with the new_process_group / detach_* helpers)
  2. Check the child actually left the caller's group: its pid must differ from the caller's pgid
  3. If you have the caller's own pid, skip enrollment entirely rather than wrapping it

Example fix

// before
let pg = ProcessGroup::new(std::process::id())?; // refuses: own group
// after
let child = spawn_detached(cmd)?; // child runs in its own process group
let pg = ProcessGroup::new(child.id())?;
Defensive patterns

Strategy: validation

Validate before calling

fn not_own_group(pid: u32) -> bool {
    i64::from(pid) != i64::from(nix::unistd::getpgrp().as_raw())
}
assert!(not_own_group(child_pid), "cannot enroll caller's own process group");

Type guard

fn not_own_group(pid: u32) -> bool {
    i64::from(pid) != i64::from(nix::unistd::getpgrp().as_raw())
}

Try / catch

if let Err(e) = ProcessGroup::new(pid) {
    if e.kind() == io::ErrorKind::InvalidInput {
        log::error!("pid {pid} is the caller's own process group; skipping kill enrollment");
        return Ok(()); // nothing to clean up
    }
    return Err(e.into());
}

Prevention

When it happens

Trigger: Enrolling the caller's own pid or the pid of a process already sharing the caller's process group — e.g. passing std::process::id(), or a child that never actually moved to a new group (setpgid not called/failed).

Common situations: Calling the API from the top-level process instead of from the parent of a detached child; a spawn helper that silently failed to create the new group; testing with the current process's pid.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/71824a3ebf023460. Report an issue: GitHub.