ogham/exa · error · io::Error

Error: path somehow contained a NUL?

Error message

Error: path somehow contained a NUL?

What it means

This io::Error comes from exa's extended-attribute module (src/fs/feature/xattr.rs). Before calling the OS listxattr functions, exa converts the file path to a C string; the conversion is rejected when path.to_str() returns None (the path is not valid UTF-8) or when CString::new finds an interior NUL byte. The message names only the NUL case, so it is misleading: on Unix a NUL byte cannot appear in a real filename, and in practice this error almost always means the filename contained non-UTF-8 bytes. It is returned as an Err of kind ErrorKind::Other, not a panic, so the caller sees it as a failed attributes() / list_attrs() call.

Source

Thrown at src/fs/feature/xattr.rs:64

    No,
}

/// Extended attribute
#[derive(Debug, Clone)]
pub struct Attribute {
    pub name: String,
    pub size: usize,
}


#[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn list_attrs(lister: &lister::Lister, path: &Path) -> io::Result<Vec<Attribute>> {
    use std::ffi::CString;

    let c_path = match path.to_str().and_then(|s| CString::new(s).ok()) {
        Some(cstring) => cstring,
        None => {
            return Err(io::Error::new(io::ErrorKind::Other, "Error: path somehow contained a NUL?"));
        }
    };

    let bufsize = lister.listxattr_first(&c_path);
    match bufsize.cmp(&0) {
        Ordering::Less     => return Err(io::Error::last_os_error()),
        Ordering::Equal    => return Ok(Vec::new()),
        Ordering::Greater  => {},
    }

    let mut buf = vec![0_u8; bufsize as usize];
    let err = lister.listxattr_second(&c_path, &mut buf, bufsize);

    match err.cmp(&0) {
        Ordering::Less     => return Err(io::Error::last_os_error()),
        Ordering::Equal    => return Ok(Vec::new()),
        Ordering::Greater  => {},
    }

View on GitHub (pinned to 3d1edbb470)

Solutions

  1. If you are the library consumer: validate or fix the filename first. Rename the offending file to a valid UTF-8 name, or run exa in a locale/terminal that handles the bytes, e.g. with LC_ALL=C or a UTF-8 locale so the name round-trips.
  2. If you hit this as a user of the exa binary: drop --extended for that directory, or rename the file (find it with: find . -name '*[! -~]*' or ls -b to show escapes).
  3. If you maintain this code: build the CString from the raw OS bytes instead of a &str, so only real NUL bytes fail. Use std::os::unix::ffi::OsStrExt::as_bytes, and return io::ErrorKind::InvalidInput with an accurate message distinguishing the non-UTF-8 and NUL cases.
  4. As a defensive change, map the failure to the OS convention: io::Error::from_raw_os_error(libc::EINVAL) so callers can match on the error kind.

Example fix

// before (src/fs/feature/xattr.rs)
let c_path = match path.to_str().and_then(|s| CString::new(s).ok()) {
    Some(cstring) => cstring,
    None => return Err(io::Error::new(io::ErrorKind::Other, "Error: path somehow contained a NUL?")),
};

// after: use the raw OS bytes; only a real NUL byte can fail now
#[cfg(unix)]
let c_path = {
    use std::os::unix::ffi::OsStrExt;
    match CString::new(path.as_os_str().as_bytes()) {
        Ok(cstring) => cstring,
        Err(_) => return Err(io::Error::new(io::ErrorKind::InvalidInput,
                                           "path contains an interior NUL byte")),
    }
};
Defensive patterns

Strategy: validation

Validate before calling

// Before calling attributes()/symlink_attributes() on a user-supplied path:
use std::ffi::OsStr;
use std::path::Path;

fn check_path_for_xattrs(path: &Path) -> Result<(), std::io::Error> {
    // Case 1: not valid UTF-8 (the common real-world trigger for this message)
    if path.to_str().is_none() {
        return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput,
            format!("path is not valid UTF-8: {:?}", path.as_os_str())));
    }
    // Case 2: an actual interior NUL byte (only possible in synthetic paths)
    if path.to_str().map_or(false, |s| s.contains('\0')) {
        return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput,
            "path contains an interior NUL byte"));
    }
    Ok(())
}

Type guard

fn is_xattr_listable(path: &Path) -> bool {
    // Mirror of the check in src/fs/feature/xattr.rs list_attrs():
    // the CString conversion must succeed.
    path.to_str().map_or(false, |s| !s.contains('\0'))
}

Try / catch

// Treat attribute listing as optional metadata, not a fatal error:
match path.attributes() {
    Ok(attrs) => { /* render xattrs */ }
    Err(e) if e.kind() == std::io::ErrorKind::Other
            && e.to_string().contains("NUL") => {
        // non-UTF-8 or NUL-containing path: skip extended attributes for this file
    }
    Err(e) => { /* report or skip */ }
}

Prevention

When it happens

Trigger: Calling Path::attributes() or Path::symlink_attributes() (the FileAttributes trait), or running exa with --extended/--all with xattrs enabled, on a Linux/macOS system where a listed filename has bytes that are not valid UTF-8 (for example latin-1 names or a byte sequence that is invalid UTF-8). The same match arm also fires for a path that truly contains an embedded NUL, which essentially only happens for programmatically built paths, not real directory entries.

Common situations: Listing old archives, media directories, or files created by non-UTF-8 programs (Windows latin-1 names copied via SMB, misconfigured locale). Also triggered when a wrapper script builds paths from unvalidated input. Note the source loses the original bytes: it goes through path.to_str(), so any non-UTF-8 path is reported with the wrong 'NUL' message.

Related errors


AI-assisted analysis of ogham/exa@3d1edbb470 (2026-08-16). Data as JSON: /api/errors/02280ecba9c28e30. Report an issue: GitHub.