Hmbown/CodeWhale · error · io::Error

external credential path contains a NUL byte

Error message

external credential path contains a NUL byte

What it means

Path components passed to `open_secure_regular_file` are converted to `CString` for `openat` syscalls. Since NUL bytes cannot appear inside a real filesystem component name, any embedded NUL means the path string is malformed or hostile, and the opener rejects it with this `InvalidInput` error before making any syscall.

Solutions

  1. Sanitize/validate the path string before use: reject or strip any `\0` bytes at the source of the value.
  2. If the path came from a fixed-size buffer or C FFI, trim at the first NUL instead of treating the padding as part of the name.
  3. Fix the code/config that produced the malformed string — a NUL in a path is never legitimate.
  4. Log the offending value's bytes to identify where the NUL is being introduced.

Example fix

// before
let path = String::from_utf8_lossy(&raw_buf).to_string(); // may contain \0
let creds = read_to_string(path)?;
// after
let path = String::from_utf8_lossy(&raw_buf).trim_end_matches('\0').to_string();
assert!(!path.contains('\0'));
let creds = read_to_string(path)?;
Defensive patterns

Strategy: validation

Validate before calling

if path.as_os_str().as_encoded_bytes().contains(&0) {
    return Err(anyhow::anyhow!("credential path contains NUL byte"));
}

Type guard

fn is_nul_free(p: &Path) -> bool {
    !p.as_os_str().as_encoded_bytes().contains(&0)
}

Try / catch

match read_to_string(&cred_path) {
    Err(e) if e.to_string().contains("NUL byte") => {
        eprintln!("Malformed credential path (embedded NUL) in {cred_path:?}; check input source.");
    }
    other => other?,
}

Prevention

When it happens

Trigger: `read_to_string`/`read_codewhale_owned_to_string` called with a path whose bytes contain a NUL character (e.g. a truncated/concatenated string, or untrusted input with `\0`).

Common situations: Untrusted or interpolated paths containing raw NULs; buffers built from fixed-size C-style strings that kept a trailing NUL; injection attempts where validation is the intended defense.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/6f0884c70a2f9644. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/external_credentials.rs:185

    let mut normals = path
        .components()
        .filter_map(|component| match component {
            Component::Normal(part) => Some(Ok(part)),
            Component::RootDir => None,
            Component::Prefix(_) | Component::CurDir | Component::ParentDir => {
                Some(Err(io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "external credential path must be lexically normalized",
                )))
            }
        })
        .peekable();

    let mut opened_leaf = false;
    while let Some(component) = normals.next() {
        let component = component?;
        let component = CString::new(component.as_bytes()).map_err(|_| {
            io::Error::new(
                io::ErrorKind::InvalidInput,
                "external credential path contains a NUL byte",
            )
        })?;
        let leaf = normals.peek().is_none();
        #[cfg(test)]
        if leaf {
            BEFORE_LEAF_OPEN_HOOK.with(|hook| {
                if let Some(hook) = hook.borrow_mut().take() {
                    hook();
                }
            });
        }
        let flags = if leaf {
            libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK
        } else {
            libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_DIRECTORY
        };

View on GitHub (pinned to 73e0f67d83)