Hmbown/CodeWhale · error

failed to inherit reviewed plugin executable descriptor

Error message

failed to inherit reviewed plugin executable descriptor

What it means

On Unix, the reviewed-plugin launcher keeps the verified executable's file descriptor open across execve by clearing FD_CLOEXEC via fcntl(F_SETFD), then spawns through /proc/self/fd/N (or /dev/fd/N). This error means fcntl(F_GETFD) or fcntl(F_SETFD) returned a negative result on that descriptor (crates/tui/src/mcp.rs:924-927), so descriptor-based launch cannot be guaranteed and the spawn aborts before any exec.

Source

Thrown at crates/tui/src/mcp.rs:926

            .iter()
            .map(|byte| format!("{byte:02x}"))
            .collect::<String>();
        if &actual != expected {
            anyhow::bail!("reviewed plugin executable bytes changed before spawn");
        }
        file.seek(std::io::SeekFrom::Start(0))
            .context("rewind reviewed launch file after verification")?;

        #[cfg(unix)]
        let launch_path = {
            use std::os::fd::AsRawFd as _;
            let fd = file.as_raw_fd();
            // SAFETY: `fd` is owned by `file`; clearing only FD_CLOEXEC keeps
            // that same descriptor available across the imminent exec.
            let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
            if flags < 0 || unsafe { libc::fcntl(fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC) } < 0
            {
                anyhow::bail!("failed to inherit reviewed plugin executable descriptor");
            }
            #[cfg(target_os = "linux")]
            let prefix = "/proc/self/fd";
            #[cfg(not(target_os = "linux"))]
            let prefix = "/dev/fd";
            std::ffi::OsString::from(format!("{prefix}/{fd}"))
        };

        #[cfg(not(unix))]
        let launch_path = path.as_os_str().to_os_string();

        self.opened_files.push(file);
        Ok(launch_path)
    }

    fn bind_cwd(&mut self, cwd: &Path) -> Result<()> {
        #[cfg(unix)]
        {

View on GitHub (pinned to 8880682c63)

Solutions

  1. Retry the connection once - a fresh open yields a fresh descriptor and normally succeeds.
  2. Check fd pressure: ulimit -n and the count in /proc/<pid>/fd; raise the limit if near exhaustion.
  3. Audit embedding code for loops that close foreign descriptors (pre-exec hygiene like `for fd in 3..N { close(fd) }`) and exclude this one.
  4. If it reproduces under a sandbox/seccomp profile, permit fcntl(F_GETFD/F_SETFD) or report a bug with an strace.

Example fix

// before: single attempt; a rare EBADF aborts plugin spawn
let conn = McpConnection::connect_with_policy(name, cfg, &timeouts, policy).await?;
// after: retry once - descriptor races are transient
let conn = match McpConnection::connect_with_policy(name, cfg.clone(), &timeouts, policy).await {
    Ok(conn) => conn,
    Err(err) if err.to_string().contains("failed to inherit reviewed plugin") => {
        McpConnection::connect_with_policy(name, cfg, &timeouts, policy).await?
    }
    Err(err) => return Err(err),
};
Defensive patterns

Strategy: try-catch

Try / catch

let conn = match McpConnection::connect_with_policy(name, cfg.clone(), &timeouts, policy).await {
    Ok(conn) => conn,
    Err(err) if err.to_string().contains("failed to inherit reviewed plugin") => {
        // Transient descriptor race: one retry on a freshly opened file.
        McpConnection::connect_with_policy(name, cfg, &timeouts, policy).await?
    }
    Err(err) => return Err(err),
};

Prevention

When it happens

Trigger: fcntl failing with EBADF because the descriptor was closed concurrently by another thread/task, fd-table pressure, or a seccomp/sandbox profile denying fcntl. Cannot fire in normal operation.

Common situations: Embedders or test harnesses that aggressively close inherited fds before spawn; restrictive containers (gVisor, seccomp filters); fd-limit exhaustion with many concurrent MCP connections.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/c575d9b3e173d45d. Report an issue: GitHub.