openai/codex · error · io::Error
PermissionDenied
PermissionDenied
Error message
descriptor-backed mount does not match its destination: {} What it means
This is the anti-tamper core of fd mount authentication: the file opened on the descriptor must be the very same inode the destination path names. verify_fd_mounts compares (dev, ino) from the descriptor's metadata against symlink_metadata of the destination; a mismatch means descriptor and destination disagree, which is treated as tampering and rejected with ErrorKind::PermissionDenied.
Source
Thrown at codex-rs/linux-sandbox/src/fd_mount.rs:72
}
let destination = Path::new(destination);
if !destination.is_absolute() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"descriptor-backed mount destination must be absolute: {}",
destination.display()
),
));
}
let descriptor_metadata = file.metadata()?;
let destination_metadata = fs::symlink_metadata(destination)?;
if (descriptor_metadata.dev(), descriptor_metadata.ino())
!= (destination_metadata.dev(), destination_metadata.ino())
{
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"descriptor-backed mount does not match its destination: {}",
destination.display()
),
));
}
// Closing immediately prevents a writable host directory descriptor
// from reaching bridge workers or the sandboxed command.
drop(file);
}
Ok(())
}
#[cfg(test)]
#[path = "fd_mount_tests.rs"]View on GitHub (pinned to 339751715c)
Solutions
- Open the destination itself and pass that exact handle (File::open(dest)) so identity is guaranteed
- Re-create the fd after any rename or replacement of the destination
- Do not use a symlink as DEST when the fd refers to the target file; resolve symlinks first
Example fix
// before -- fd from a staged copy, destination is the real path
let file = File::open("/tmp/socket-root.staged")?;
let spec = format!("{}:/tmp/socket-root", file.as_raw_fd());
// after -- open the destination itself so dev/ino match
let dest = "/tmp/socket-root";
let file = File::open(dest)?;
let spec = format!("{}:{}", file.as_raw_fd(), dest); Defensive patterns
Strategy: validation
Validate before calling
fn fd_matches_destination(file: &std::fs::File, dest: &std::path::Path) -> bool {
use std::os::unix::fs::MetadataExt;
match (file.metadata(), std::fs::symlink_metadata(dest)) {
(Ok(a), Ok(b)) => (a.dev(), a.ino()) == (b.dev(), b.ino()),
_ => false,
}
}
for (file, dest) in &pairs {
if !fd_matches_destination(file, dest) {
return Err(format!("fd/destination identity mismatch: {}", dest.display()));
}
} Type guard
fn fd_matches_destination(file: &std::fs::File, dest: &std::path::Path) -> bool {
use std::os::unix::fs::MetadataExt;
match (file.metadata(), std::fs::symlink_metadata(dest)) {
(Ok(a), Ok(b)) => (a.dev(), a.ino()) == (b.dev(), b.ino()),
_ => false,
}
} Try / catch
match verify_fd_mounts(&mounts) {
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
// destination was replaced: re-open it and rebuild the specs
rebuild_specs_from_destinations()?;
}
other => other?,
} Prevention
- Create the fd and the destination string from the same open() call
- Never rename or replace a mount destination while a sandbox launch is in flight
- Treat PermissionDenied from this check as a security signal, not a transient error
When it happens
Trigger: An fd opened from a different file than the destination names; the destination renamed or replaced between fd creation and verification; the destination being a symlink, since symlink_metadata stats the link itself and can never match a regular file's inode.
Common situations: A daemon recreates its socket or tmpdir after the launcher captured the fd; test harnesses that swap files via rename(); pointing the fd at a staged copy while the destination names the original.
Related errors
- InvalidInput
- invalid legacy bubblewrap fd mount: {error}
- bubblewrap is unavailable: no system bwrap was found on PATH
- generated image directory is not a real directory
- failed to open bundled bubblewrap {}: {err}
AI-assisted analysis of openai/codex@339751715c (2026-08-25).
Data as JSON: /api/errors/53a016a306f39278.
Report an issue: GitHub.