uutils/coreutils · error · io::Error

safe-traversal-error-invalid-fd

Error message

safe-traversal-error-invalid-fd

What it means

`DirFd::from_raw_fd` validates that the caller-supplied raw file descriptor is non-negative before taking ownership. A negative fd is invalid (and would make `OwnedFd::from_raw_fd` UB), so it returns `InvalidInput` with the localized "safe-traversal-error-invalid-fd" message.

Source

Thrown at src/uucore/src/lib/features/safe_traversal.rs:481

        // target truncated. Callers reach here right after unlinking `name`,
        // which is precisely the window an attacker races.
        let flags = OFlag::O_CREAT
            | OFlag::O_WRONLY
            | OFlag::O_TRUNC
            | OFlag::O_CLOEXEC
            | OFlag::O_NOFOLLOW;
        let mode = Mode::from_bits_truncate(0o666); // Default file permissions

        let fd: OwnedFd = openat(self.fd.as_fd(), name_cstr.as_c_str(), flags, mode)
            .map_err(|e| io::Error::from_raw_os_error(e as i32))?;

        Ok(fs::File::from(fd))
    }

    /// Create a DirFd from an existing file descriptor (takes ownership)
    pub fn from_raw_fd(fd: RawFd) -> io::Result<Self> {
        if fd < 0 {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                translate!("safe-traversal-error-invalid-fd"),
            ));
        }
        // SAFETY: We've verified fd >= 0, and the caller is transferring ownership
        let owned_fd = unsafe { OwnedFd::from_raw_fd(fd) };
        Ok(Self { fd: owned_fd })
    }
}

/// Find the deepest existing directory ancestor for a path.
///
/// Returns the existing ancestor path and a list of components that need to be created.
/// Uses `metadata` (follows symlinks) so that symlinks to directories are treated as
/// existing ancestors rather than components to create.
fn find_existing_ancestor(path: &Path) -> io::Result<(PathBuf, Vec<OsString>)> {
    let mut current = path.to_path_buf();
    let mut components: Vec<OsString> = Vec::new();

View on GitHub (pinned to 85295bbf78)

Solutions

  1. Check the raw fd >= 0 (i.e. the underlying open succeeded) before calling from_raw_fd
  2. Return/propagate the original open error (errno) instead of the fd
  3. Prefer APIs returning Result<OwnedFd> so the invalid case can't reach from_raw_fd

Example fix

// before
let fd = unsafe { libc::open(path.as_ptr(), flags) };
DirFd::from_raw_fd(fd)?;
// after
let fd = unsafe { libc::open(path.as_ptr(), flags) };
if fd < 0 { return Err(io::Error::last_os_error()); }
DirFd::from_raw_fd(fd)?;
Defensive patterns

Strategy: validation

Validate before calling

fn safe_from_raw_fd(fd: std::os::fd::RawFd) -> io::Result<DirFd> {
    if fd < 0 { return Err(io::Error::last_os_error()); }
    DirFd::from_raw_fd(fd)
}

Type guard

fn is_valid_fd(fd: i32) -> bool { fd >= 0 }

Try / catch

match DirFd::from_raw_fd(fd) {
    Err(e) if e.kind() == io::ErrorKind::InvalidInput => {
        return Err(io::Error::last_os_error());
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling `DirFd::from_raw_fd(-1)` — typically after an `open`/`openat` call returned -1 on error and the raw fd was forwarded without checking.

Common situations: FFI wrappers that forget to check the -1 error return of libc open/openat, scripts binding native traversal APIs, refactored code losing an error check.

Related errors


AI-assisted analysis of uutils/coreutils@85295bbf78 (2026-08-31). Data as JSON: /api/errors/fb108d5810849d23. Report an issue: GitHub.