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
- 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).
- Audit for early drops or double closes of the preserved handles (RAII review, fd counting with lsof).
- 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
- Move (do not clone-and-drop) preserved Files into the launch call so they stay open until exec.
- Never share preserved descriptors with code that may drop or close them concurrently.
- Treat EBADF on your own fd list as an ownership bug, not a transient error.
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
- failed to clear CLOEXEC for preserved bubblewrap file descri
- failed to open bundled bubblewrap {}: {err}
- invalid bundled bubblewrap fd path: {err}
- failed to exec bundled bubblewrap {} via {fd_path}: {err}
- failed to normalize bundled bubblewrap path {}: {err}
AI-assisted analysis of openai/codex@339751715c (2026-08-25).
Data as JSON: /api/errors/73e603f756cf39f3.
Report an issue: GitHub.