{"record":{"id":"45de48ad7a7568ad","repo":"astrid-runtime/astrid","slug":"xattr-path-has-nul","errorCode":null,"errorMessage":"xattr path has NUL","messagePattern":"xattr path has NUL","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-vfs/src/workspace_cow/overlayfs.rs","lineNumber":464,"sourceCode":"                     ({name} on {}); promoting it would leave stale lower entries. This \\\n                     case is not yet supported, so the promote is refused — roll back \\\n                     instead of committing an incorrect tree.\",\n                    path.display()\n                )));\n            }\n        }\n        ensure_no_opaque_markers(&path)?;\n    }\n    Ok(())\n}\n\n/// Presence check for a single extended attribute via `lgetxattr(2)`.\n/// `Ok(true)` = the attribute exists; `ENODATA`/`ENOTSUP` → `Ok(false)` (no such\n/// marker, or a filesystem without xattrs); any other error propagates so the\n/// caller fails closed.\nfn has_xattr(path: &Path, name: &str) -> io::Result<bool> {\n    let path_c = CString::new(path.as_os_str().as_bytes())\n        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, \"xattr path has NUL\"))?;\n    let name_c = CString::new(name)\n        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, \"xattr name has NUL\"))?;\n    // SAFETY: both C strings are NUL-terminated and outlive the call; a null\n    // value pointer with size 0 asks only for the current value size and writes\n    // nothing. `lgetxattr` does not follow a final symlink.\n    let rc = unsafe { libc::lgetxattr(path_c.as_ptr(), name_c.as_ptr(), std::ptr::null_mut(), 0) };\n    if rc >= 0 {\n        return Ok(true);\n    }\n    let err = io::Error::last_os_error();\n    match err.raw_os_error() {\n        Some(libc::ENODATA | libc::ENOTSUP) => Ok(false),\n        _ => Err(err),\n    }\n}\n\n/// A short, deterministic hex digest of a path, used only as a directory name.\nfn path_hash(path: &Path) -> String {","sourceCodeStart":446,"sourceCodeEnd":482,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-vfs/src/workspace_cow/overlayfs.rs#L446-L482","documentation":"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.)","triggerScenarios":"ensure_no_opaque_markers calls has_xattr with a workspace/overlay path containing a 0x00 byte, typically from corrupted or untrusted path input.","commonSituations":"Corrupted overlay directory state; paths reconstructed from binary storage or deserialized data containing NUL; untrusted input concatenated into path strings.","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"],"exampleFix":"// before\nhas_xattr(&path, \"trusted.overlay.opaque\")?;\n// after\nif path.as_os_str().as_bytes().contains(&0) {\n    return Err(io::Error::new(io::ErrorKind::InvalidInput, \"NUL in xattr path\"));\n}\nhas_xattr(&path, \"trusted.overlay.opaque\")?;","handlingStrategy":"validation","validationCode":"fn ensure_xattr_path_safe(path: &Path, name: &str) -> io::Result<()> {\n    if path.as_os_str().as_bytes().contains(&0) {\n        return Err(io::Error::new(io::ErrorKind::InvalidInput, \"NUL in xattr path\"));\n    }\n    if name.contains('\\0') {\n        return Err(io::Error::new(io::ErrorKind::InvalidInput, \"NUL in xattr name\"));\n    }\n    Ok(())\n}","typeGuard":"fn is_c_string_safe(p: &Path) -> bool {\n    !p.as_os_str().as_bytes().contains(&0)\n}","tryCatchPattern":"match ensure_no_opaque_markers(&overlay_dir) {\n    Err(e) if e.kind() == io::ErrorKind::InvalidInput => {\n        log::error!(\"corrupted overlay path: {:?}\", overlay_dir.as_os_str().as_bytes());\n        return Err(e);\n    }\n    other => other,\n}","preventionTips":["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"],"tags":["linux","xattr","path","ffi"],"backgroundTag":"invalid-argument-value","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}