openai/codex · critical

failed to read fd flags for preserved bubblewrap file descri

Error message

failed to read fd flags for preserved bubblewrap file descriptor {fd}: {err}

What it means

File descriptors passed as preserved_files must survive execv into the sandbox, so make_files_inheritable clears FD_CLOEXEC on each. clear_cloexec first reads the descriptor flags with fcntl(fd, F_GETFD); a negative return (typically EBADF, meaning the fd is already closed) panics before any flags can be changed.

Source

Thrown at codex-rs/linux-sandbox/src/exec_util.rs:27

            Ok(value) => cstrings.push(value),
            Err(err) => panic!("failed to convert argv to CString: {err}"),
        }
    }
    cstrings
}

pub(crate) fn make_files_inheritable(files: &[File]) {
    for file in files {
        clear_cloexec(file.as_raw_fd());
    }
}

fn clear_cloexec(fd: libc::c_int) {
    // SAFETY: `fd` is an owned descriptor kept alive by `files`.
    let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
    if flags < 0 {
        let err = std::io::Error::last_os_error();
        panic!("failed to read fd flags for preserved bubblewrap file descriptor {fd}: {err}");
    }
    let cleared_flags = flags & !libc::FD_CLOEXEC;
    if cleared_flags == flags {
        return;
    }

    // SAFETY: `fd` is valid and we are only clearing FD_CLOEXEC.
    let result = unsafe { libc::fcntl(fd, libc::F_SETFD, cleared_flags) };
    if result < 0 {
        let err = std::io::Error::last_os_error();
        panic!("failed to clear CLOEXEC for preserved bubblewrap file descriptor {fd}: {err}");
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;

View on GitHub (pinned to 339751715c)

Solutions

  1. Keep every preserved File owned and in scope through the sandbox launch call (exec does not return on success, so ownership must outlive the call).
  2. Audit for early drops or double closes of the preserved handles (RAII review, fd counting with lsof).
  3. Ensure no other thread closes the same descriptors during launch.

Example fix

// before: both handles dropped before launch, fd invalid by exec time
let preserved = {
    let file = File::open(&path)?;
    vec![file.try_clone()?, file]
}; // dropped here -> EBADF panic in clear_cloexec
launcher.exec(argv, preserved);

// after: keep ownership until the call
let file = File::open(&path)?;
launcher.exec(argv, vec![file]);
Defensive patterns

Strategy: validation

Validate before calling

use std::os::fd::AsRawFd;
fn fds_valid(files: &[std::fs::File]) -> bool {
    files.iter().all(|f| {
        let fd = f.as_raw_fd();
        // SAFETY: read-only descriptor flag query on an fd we own
        unsafe { libc::fcntl(fd, libc::F_GETFD) } >= 0
    })
}
assert!(fds_valid(&preserved));

Prevention

When it happens

Trigger: A File in preserved_files was dropped or closed before the sandbox exec, so its fd number is invalid when F_GETFD runs; a double close of the same handle; another thread closing shared descriptors concurrently.

Common situations: RAII scope of a preserved File ending before the launch call; cloning and dropping File handles across task or thread boundaries; ownership bugs where the caller believes it still holds the descriptor.

Related errors


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