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

  1. Validate both swap paths for NUL bytes before calling promote
  2. Repair the upstream producer of the corrupted path value
  3. Guard with path.as_os_str().as_bytes().contains(&0) checks in caller code
  4. 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

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


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)