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
- Sanitize/validate the path string before use: reject or strip any `\0` bytes at the source of the value.
- 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.
- Fix the code/config that produced the malformed string — a NUL in a path is never legitimate.
- 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
- Trim NUL terminators when importing paths from C buffers or fixed-size fields.
- Validate untrusted strings for control characters before using them as paths.
- Use Rust String/PathBuf throughout — don't round-trip paths through C-style arrays.
- Log offending byte values to trace where the NUL enters.
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
- Codewhale-owned credential file must be singly linked
- Codewhale-owned credential file must be singly linked…
- external credential path must be absolute
- external credential path must be lexically normalized
- external credential path must name a non-reparse regular…
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)