{"record":{"id":"0637f463623c82b6","repo":"astrid-runtime/astrid","slug":"clone-dest-path-has-nul","errorCode":null,"errorMessage":"clone dest path has NUL","messagePattern":"clone dest path has NUL","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-vfs/src/workspace_cow/apfs.rs","lineNumber":228,"sourceCode":"/// A short, deterministic hex digest of a path, used only as a directory name.\nfn path_hash(path: &Path) -> String {\n    use std::collections::hash_map::DefaultHasher;\n    use std::hash::{Hash, Hasher};\n    // Canonicalize when possible so the same workspace maps to the same digest\n    // regardless of how it was addressed; fall back to the raw path otherwise.\n    let key = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());\n    let mut hasher = DefaultHasher::new();\n    key.hash(&mut hasher);\n    format!(\"{:016x}\", hasher.finish())\n}\n\n/// `clonefile(src, dst, 0)` — copy-on-write clone of a whole directory tree.\n/// `dst` must not already exist.\nfn clonefile(src: &Path, dst: &Path) -> io::Result<()> {\n    let src_c = CString::new(src.as_os_str().as_bytes())\n        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, \"clone source path has NUL\"))?;\n    let dst_c = CString::new(dst.as_os_str().as_bytes())\n        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, \"clone dest path has NUL\"))?;\n    // SAFETY: `src_c`/`dst_c` are valid, NUL-terminated C strings that outlive\n    // the call; `clonefile` reads them and returns a status code, retaining no\n    // pointers. Flag `0` = default (clone contents, don't follow the final\n    // symlink).\n    let rc = unsafe { libc::clonefile(src_c.as_ptr(), dst_c.as_ptr(), 0) };\n    if rc != 0 {\n        return Err(io::Error::last_os_error());\n    }\n    Ok(())\n}\n\n/// `renamex_np(a, b, RENAME_SWAP)` — atomically swap two existing paths on the\n/// same volume.\nfn renamex_swap(a: &Path, b: &Path) -> io::Result<()> {\n    let a_c = CString::new(a.as_os_str().as_bytes())\n        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, \"swap path a has NUL\"))?;\n    let b_c = CString::new(b.as_os_str().as_bytes())\n        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, \"swap path b has NUL\"))?;","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-vfs/src/workspace_cow/apfs.rs#L210-L246","documentation":"clonefile converts both src and dst Paths to CStrings for the clonefile(2) syscall; the destination conversion has its own error arm. If the destination path contains an interior NUL byte, CString::new fails and the library raises InvalidInput with this message. The destination must also not already exist for clonefile to succeed.","triggerScenarios":"Calling prepare/promote/rollback where the target/destination workspace path contains a 0x00 byte from untrusted or corrupted input.","commonSituations":"Corrupted config or state files supplying the destination path; NUL injected via deserialized data; paths assembled from untrusted user input without validation.","solutions":["Validate the destination path for NUL bytes before invoking CoW operations","Fix the upstream source that produced the corrupted destination path","Add an early check: if path.as_os_str().as_bytes().contains(&0) return a clear caller-side error","Log the raw destination bytes to locate the corruption"],"exampleFix":"// before\nclonefile(&src, &dst)?;\n// after\nif dst.as_os_str().as_bytes().contains(&0) {\n    return Err(io::Error::new(io::ErrorKind::InvalidInput, \"bad dst path\"));\n}\nclonefile(&src, &dst)?;","handlingStrategy":"validation","validationCode":"fn ensure_dst_safe(dst: &Path) -> io::Result<()> {\n    if dst.as_os_str().as_bytes().contains(&0) {\n        Err(io::Error::new(io::ErrorKind::InvalidInput, \"dst contains NUL\"))\n    } else if dst.exists() {\n        Err(io::Error::new(io::ErrorKind::AlreadyExists, \"clone dst exists\"))\n    } else { Ok(()) }\n}","typeGuard":"fn is_c_string_safe(p: &Path) -> bool {\n    !p.as_os_str().as_bytes().contains(&0)\n}","tryCatchPattern":"match clonefile(&src, &dst) {\n    Err(e) if e.kind() == io::ErrorKind::InvalidInput => {\n        log::error!(\"bad clone dst path: {:?}\", dst.as_os_str().as_bytes());\n        return Err(e);\n    }\n    other => other,\n}","preventionTips":["Validate destination paths before any FFI-based filesystem operation","Remember clonefile requires dst not to exist — pre-check with Path::exists","Sanitize paths coming from config or IPC at ingestion time","Use typed newtypes for validated paths to push checks to construction"],"tags":["macos","apfs","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"}