astrid-runtime/astrid · error

clone source path has NUL

Error message

clone source path has NUL

What it means

clonefile wraps the macOS clonefile(2) syscall and must convert Rust Paths to NUL-terminated C strings via CString::new. If the source path contains an interior NUL byte, CString::new fails and the library raises InvalidInput with this message. Such paths cannot be passed to any C API.

Solutions

  1. Sanitize/validate workspace paths before calling CoW operations: reject any path containing a NUL byte
  2. Fix the upstream producer that embedded NUL into the path string
  3. Use Path::to_str() and check !contains('\0') as an early guard
  4. Log the offending raw path bytes to find the corruption source

Example fix

// before
clonefile(&src, &dst)?;
// after: validate first
let s = src.to_str().ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "non-UTF8 path"))?;
assert!(!s.contains('\0'), "path contains NUL");
clonefile(&src, &dst)?;
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_no_nul(p: &Path) -> io::Result<()> {
    if p.as_os_str().as_bytes().contains(&0) {
        Err(io::Error::new(io::ErrorKind::InvalidInput, "path contains NUL"))
    } else { Ok(()) }
}

Type guard

fn is_c_string_safe(p: &Path) -> bool {
    !p.as_os_str().as_bytes().contains(&0)
}

Try / catch

match clonefile(&src, &dst) {
    Err(e) if e.kind() == io::ErrorKind::InvalidInput && e.to_string().contains("NUL") => {
        log::error!("rejecting corrupted path: {:?}", src.as_os_str().as_bytes());
        sanitize_and_retry()
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling prepare/promote/rollback with a workspace source path that contains a 0x00 byte, typically from untrusted or corrupted input assembled into a PathBuf.

Common situations: Deserializing paths from JSON/config where escaped \u0000 slipped through; corrupted database/config values; paths built by concatenating untrusted strings; binary data mistakenly treated as path text.

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/dd5313907ab7af7f. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-vfs/src/workspace_cow/apfs.rs:226

}

/// 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};
    // Canonicalize when possible so the same workspace maps to the same digest
    // regardless of how it was addressed; fall back to the raw path otherwise.
    let key = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
    let mut hasher = DefaultHasher::new();
    key.hash(&mut hasher);
    format!("{:016x}", hasher.finish())
}

/// `clonefile(src, dst, 0)` — copy-on-write clone of a whole directory tree.
/// `dst` must not already exist.
fn clonefile(src: &Path, dst: &Path) -> io::Result<()> {
    let src_c = CString::new(src.as_os_str().as_bytes())
        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "clone source path has NUL"))?;
    let dst_c = CString::new(dst.as_os_str().as_bytes())
        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "clone dest path has NUL"))?;
    // SAFETY: `src_c`/`dst_c` are valid, NUL-terminated C strings that outlive
    // the call; `clonefile` reads them and returns a status code, retaining no
    // pointers. Flag `0` = default (clone contents, don't follow the final
    // symlink).
    let rc = unsafe { libc::clonefile(src_c.as_ptr(), dst_c.as_ptr(), 0) };
    if rc != 0 {
        return Err(io::Error::last_os_error());
    }
    Ok(())
}

/// `renamex_np(a, b, RENAME_SWAP)` — atomically swap two existing paths on the
/// same volume.
fn renamex_swap(a: &Path, b: &Path) -> io::Result<()> {
    let a_c = CString::new(a.as_os_str().as_bytes())
        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "swap path a has NUL"))?;

View on GitHub (pinned to affd8760f4)