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

  1. Open the destination itself and pass that exact handle (File::open(dest)) so identity is guaranteed
  2. Re-create the fd after any rename or replacement of the destination
  3. 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

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


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