{"record":{"id":"b789b69a25c0b3ee","repo":"astrid-runtime/astrid","slug":"swap-path-a-has-nul","errorCode":null,"errorMessage":"swap path a has NUL","messagePattern":"swap path a has NUL","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-vfs/src/workspace_cow/apfs.rs","lineNumber":244,"sourceCode":"        .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\"))?;\n    // SAFETY: both are valid, NUL-terminated C strings outliving the call;\n    // `renamex_np` reads them and returns a status code, retaining no pointers.\n    let rc = unsafe { libc::renamex_np(a_c.as_ptr(), b_c.as_ptr(), libc::RENAME_SWAP) };\n    if rc != 0 {\n        return Err(io::Error::last_os_error());\n    }\n    Ok(())\n}\n","sourceCodeStart":226,"sourceCodeEnd":255,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-vfs/src/workspace_cow/apfs.rs#L226-L255","documentation":"renamex_swap wraps macOS renamex_np(2) with RENAME_SWAP to atomically exchange two existing paths. Both paths must be converted to NUL-terminated C strings; if path a contains an interior NUL byte the conversion fails and the library raises InvalidInput with this message.","triggerScenarios":"Calling promote where the first swap operand (path a) contains a 0x00 byte, usually from corrupted or untrusted path input.","commonSituations":"NUL bytes introduced by deserialization or config corruption; paths built by concatenating raw untrusted strings; binary garbage read back from state storage.","solutions":["Validate both swap paths for NUL bytes before calling promote","Repair the upstream producer of the corrupted path value","Guard with path.as_os_str().as_bytes().contains(&0) checks in caller code","Log raw path bytes to trace the corruption source"],"exampleFix":"// before\nrenamex_swap(&a, &b)?;\n// after\nfor p in [&a, &b] {\n    if p.as_os_str().as_bytes().contains(&0) {\n        return Err(io::Error::new(io::ErrorKind::InvalidInput, \"NUL in swap path\"));\n    }\n}\nrenamex_swap(&a, &b)?;","handlingStrategy":"validation","validationCode":"fn ensure_swap_safe(a: &Path, b: &Path) -> io::Result<()> {\n    for p in [a, b] {\n        if p.as_os_str().as_bytes().contains(&0) {\n            return Err(io::Error::new(io::ErrorKind::InvalidInput, \"NUL in swap path\"));\n        }\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 renamex_swap(&a, &b) {\n    Err(e) if e.kind() == io::ErrorKind::InvalidInput => {\n        log::error!(\"bad swap path: {:?} / {:?}\", a, b);\n        return Err(e);\n    }\n    other => other,\n}","preventionTips":["Validate all path operands to syscall wrappers at the API boundary","Reject NUL bytes when deserializing persisted paths","Centralize path sanitization in one helper used by all CoW calls","Log raw bytes on failure for diagnosability"],"tags":["macos","path","ffi","rename"],"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"}