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
- 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
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
- 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
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
- clone source path has NUL
- swap path a has NUL
- swap path b has NUL
- mount target has NUL
- umount target has NUL
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)