astrid-runtime/astrid · error

clone dest path has NUL

Error message

clone dest path has NUL

What it means

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.

Solutions

  1. Validate the destination path for NUL bytes before invoking CoW operations
  2. Fix the upstream source that produced the corrupted destination path
  3. Add an early check: if path.as_os_str().as_bytes().contains(&0) return a clear caller-side error
  4. Log the raw destination bytes to locate the corruption

Example fix

// before
clonefile(&src, &dst)?;
// after
if dst.as_os_str().as_bytes().contains(&0) {
    return Err(io::Error::new(io::ErrorKind::InvalidInput, "bad dst path"));
}
clonefile(&src, &dst)?;
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_dst_safe(dst: &Path) -> io::Result<()> {
    if dst.as_os_str().as_bytes().contains(&0) {
        Err(io::Error::new(io::ErrorKind::InvalidInput, "dst contains NUL"))
    } else if dst.exists() {
        Err(io::Error::new(io::ErrorKind::AlreadyExists, "clone dst exists"))
    } 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 => {
        log::error!("bad clone dst path: {:?}", dst.as_os_str().as_bytes());
        return Err(e);
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling prepare/promote/rollback where the target/destination workspace path contains a 0x00 byte from untrusted or corrupted input.

Common situations: Corrupted config or state files supplying the destination path; NUL injected via deserialized data; paths assembled from untrusted user input without validation.

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

Appendix: source

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

/// 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"))?;
    let b_c = CString::new(b.as_os_str().as_bytes())
        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "swap path b has NUL"))?;

View on GitHub (pinned to affd8760f4)