astrid-runtime/astrid · error
xattr path has NUL
Error message
xattr path has NUL
What it means
has_xattr checks for an extended attribute using lgetxattr(2), which requires NUL-terminated C strings for the path and attribute name. If the path contains an interior NUL byte, CString::new fails and the library raises InvalidInput with this message before the syscall runs. (A companion error exists for NUL in the attribute name.)
Solutions
- Validate the path for NUL bytes before calling CoW/overlay operations
- Fix the upstream producer of the corrupted path
- Add a caller-side check: path.as_os_str().as_bytes().contains(&0)
- Log the raw path bytes to locate where the NUL was introduced
Example fix
// before
has_xattr(&path, "trusted.overlay.opaque")?;
// after
if path.as_os_str().as_bytes().contains(&0) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "NUL in xattr path"));
}
has_xattr(&path, "trusted.overlay.opaque")?; Defensive patterns
Strategy: validation
Validate before calling
fn ensure_xattr_path_safe(path: &Path, name: &str) -> io::Result<()> {
if path.as_os_str().as_bytes().contains(&0) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "NUL in xattr path"));
}
if name.contains('\0') {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "NUL in xattr name"));
}
Ok(())
} Type guard
fn is_c_string_safe(p: &Path) -> bool {
!p.as_os_str().as_bytes().contains(&0)
} Try / catch
match ensure_no_opaque_markers(&overlay_dir) {
Err(e) if e.kind() == io::ErrorKind::InvalidInput => {
log::error!("corrupted overlay path: {:?}", overlay_dir.as_os_str().as_bytes());
return Err(e);
}
other => other,
} Prevention
- Sanitize overlay/workspace paths when reconstructing them from persisted state
- Validate both path and xattr name for NUL before any lgetxattr-based call
- Reject NUL bytes at deserialization boundaries
- Log raw path bytes when validation fails to find the corruption source
When it happens
Trigger: ensure_no_opaque_markers calls has_xattr with a workspace/overlay path containing a 0x00 byte, typically from corrupted or untrusted path input.
Common situations: Corrupted overlay directory state; paths reconstructed from binary storage or deserialized data containing NUL; untrusted input concatenated into path strings.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- clone dest path has NUL
- clone source path has NUL
- mount target has NUL
- swap path a has NUL
- swap path b has NUL
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/45de48ad7a7568ad.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-vfs/src/workspace_cow/overlayfs.rs:464
({name} on {}); promoting it would leave stale lower entries. This \
case is not yet supported, so the promote is refused — roll back \
instead of committing an incorrect tree.",
path.display()
)));
}
}
ensure_no_opaque_markers(&path)?;
}
Ok(())
}
/// Presence check for a single extended attribute via `lgetxattr(2)`.
/// `Ok(true)` = the attribute exists; `ENODATA`/`ENOTSUP` → `Ok(false)` (no such
/// marker, or a filesystem without xattrs); any other error propagates so the
/// caller fails closed.
fn has_xattr(path: &Path, name: &str) -> io::Result<bool> {
let path_c = CString::new(path.as_os_str().as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "xattr path has NUL"))?;
let name_c = CString::new(name)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "xattr name has NUL"))?;
// SAFETY: both C strings are NUL-terminated and outlive the call; a null
// value pointer with size 0 asks only for the current value size and writes
// nothing. `lgetxattr` does not follow a final symlink.
let rc = unsafe { libc::lgetxattr(path_c.as_ptr(), name_c.as_ptr(), std::ptr::null_mut(), 0) };
if rc >= 0 {
return Ok(true);
}
let err = io::Error::last_os_error();
match err.raw_os_error() {
Some(libc::ENODATA | libc::ENOTSUP) => Ok(false),
_ => Err(err),
}
}
/// A short, deterministic hex digest of a path, used only as a directory name.
fn path_hash(path: &Path) -> String {View on GitHub (pinned to affd8760f4)