astrid-runtime/astrid · error
swap path a has NUL
Error message
swap path a has NUL
What it means
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.
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
Example fix
// before
renamex_swap(&a, &b)?;
// after
for p in [&a, &b] {
if p.as_os_str().as_bytes().contains(&0) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "NUL in swap path"));
}
}
renamex_swap(&a, &b)?; Defensive patterns
Strategy: validation
Validate before calling
fn ensure_swap_safe(a: &Path, b: &Path) -> io::Result<()> {
for p in [a, b] {
if p.as_os_str().as_bytes().contains(&0) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "NUL in swap path"));
}
}
Ok(())
} Type guard
fn is_c_string_safe(p: &Path) -> bool {
!p.as_os_str().as_bytes().contains(&0)
} Try / catch
match renamex_swap(&a, &b) {
Err(e) if e.kind() == io::ErrorKind::InvalidInput => {
log::error!("bad swap path: {:?} / {:?}", a, b);
return Err(e);
}
other => other,
} Prevention
- 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
When it happens
Trigger: Calling promote where the first swap operand (path a) contains a 0x00 byte, usually from corrupted or untrusted path input.
Common situations: NUL bytes introduced by deserialization or config corruption; paths built by concatenating raw untrusted strings; binary garbage read back from state storage.
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
- swap path b has NUL
- clone dest path has NUL
- clone source path 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/b789b69a25c0b3ee.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-vfs/src/workspace_cow/apfs.rs:244
.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"))?;
// SAFETY: both are valid, NUL-terminated C strings outliving the call;
// `renamex_np` reads them and returns a status code, retaining no pointers.
let rc = unsafe { libc::renamex_np(a_c.as_ptr(), b_c.as_ptr(), libc::RENAME_SWAP) };
if rc != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
View on GitHub (pinned to affd8760f4)