astrid-runtime/astrid · error
xattr name has NUL
Error message
xattr name has NUL
What it means
has_xattr probes an extended attribute via libc::lgetxattr, which requires NUL-terminated C strings. The xattr name passed as a Rust &str contained an embedded NUL byte, so it cannot be converted to a CString; the function fails closed with InvalidInput rather than calling the FFI with a truncated name.
Solutions
- Verify the xattr name passed to has_xattr contains no interior NUL bytes before the call
- Use only static, trusted xattr name constants (e.g. 'trusted.overlay.opaque') instead of dynamically built names
- If names come from external data, sanitize/reject any byte string containing 0x00 before use
- Log the offending name (hex-escaped) to identify where the NUL originates
Example fix
// before
let name = format!("trusted.overlay.{}", user_blob);
has_xattr(path, &name)?;
// after
if name.bytes().any(|b| b == 0) { return Err(...); }
has_xattr(path, "trusted.overlay.opaque")?; Defensive patterns
Strategy: validation
Validate before calling
fn ensure_no_nul(s: &str) -> io::Result<()> { if s.bytes().any(|b| b == 0) { Err(io::Error::new(io::ErrorKind::InvalidInput, "xattr name contains NUL")) } else { Ok(()) } } Type guard
fn is_nul_free(s: &str) -> bool { !s.as_bytes().contains(&0) } Try / catch
match has_xattr(path, name) { Err(e) if e.kind() == io::ErrorKind::InvalidInput => { /* skip/reject this xattr name */ }, Err(e) => return Err(e), Ok(has) => /* proceed */, } Prevention
- Use static string literals for xattr names
- Never build xattr names from raw byte buffers
- Validate any externally-sourced name for NUL before use
When it happens
Trigger: Calling ensure_no_opaque_markers (which calls has_xattr) on overlayfs directories when a derived xattr name contains an interior '\0' — e.g. a name built from untrusted or corrupted metadata rather than a trusted constant like 'trusted.overlay.opaque'.
Common situations: Corrupted or attacker-influenced overlay metadata producing xattr names with embedded NULs; a bug in code that assembles xattr names from byte buffers instead of static 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
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/408b088034f0e286.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-vfs/src/workspace_cow/overlayfs.rs:466
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 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};View on GitHub (pinned to affd8760f4)