astrid-runtime/astrid · error

xattr path has NUL

Error message

xattr path has NUL

What it means

has_xattr checks for an extended attribute using lgetxattr(2), which requires NUL-terminated C strings for the path and attribute name. If the path contains an interior NUL byte, CString::new fails and the library raises InvalidInput with this message before the syscall runs. (A companion error exists for NUL in the attribute name.)

Solutions

  1. Validate the path for NUL bytes before calling CoW/overlay operations
  2. Fix the upstream producer of the corrupted path
  3. Add a caller-side check: path.as_os_str().as_bytes().contains(&0)
  4. Log the raw path bytes to locate where the NUL was introduced

Example fix

// before
has_xattr(&path, "trusted.overlay.opaque")?;
// after
if path.as_os_str().as_bytes().contains(&0) {
    return Err(io::Error::new(io::ErrorKind::InvalidInput, "NUL in xattr path"));
}
has_xattr(&path, "trusted.overlay.opaque")?;
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_xattr_path_safe(path: &Path, name: &str) -> io::Result<()> {
    if path.as_os_str().as_bytes().contains(&0) {
        return Err(io::Error::new(io::ErrorKind::InvalidInput, "NUL in xattr path"));
    }
    if name.contains('\0') {
        return Err(io::Error::new(io::ErrorKind::InvalidInput, "NUL in xattr name"));
    }
    Ok(())
}

Type guard

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

Try / catch

match ensure_no_opaque_markers(&overlay_dir) {
    Err(e) if e.kind() == io::ErrorKind::InvalidInput => {
        log::error!("corrupted overlay path: {:?}", overlay_dir.as_os_str().as_bytes());
        return Err(e);
    }
    other => other,
}

Prevention

When it happens

Trigger: ensure_no_opaque_markers calls has_xattr with a workspace/overlay path containing a 0x00 byte, typically from corrupted or untrusted path input.

Common situations: Corrupted overlay directory state; paths reconstructed from binary storage or deserialized data containing NUL; untrusted input concatenated into path strings.

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

Appendix: source

Thrown at crates/astrid-vfs/src/workspace_cow/overlayfs.rs:464

                     ({name} on {}); promoting it would leave stale lower entries. This \
                     case is not yet supported, so the promote is refused — roll back \
                     instead of committing an incorrect tree.",
                    path.display()
                )));
            }
        }
        ensure_no_opaque_markers(&path)?;
    }
    Ok(())
}

/// Presence check for a single extended attribute via `lgetxattr(2)`.
/// `Ok(true)` = the attribute exists; `ENODATA`/`ENOTSUP` → `Ok(false)` (no such
/// marker, or a filesystem without xattrs); any other error propagates so the
/// caller fails closed.
fn has_xattr(path: &Path, name: &str) -> io::Result<bool> {
    let path_c = CString::new(path.as_os_str().as_bytes())
        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "xattr path has NUL"))?;
    let name_c = CString::new(name)
        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "xattr name has NUL"))?;
    // SAFETY: both C strings are NUL-terminated and outlive the call; a null
    // value pointer with size 0 asks only for the current value size and writes
    // nothing. `lgetxattr` does not follow a final symlink.
    let rc = unsafe { libc::lgetxattr(path_c.as_ptr(), name_c.as_ptr(), std::ptr::null_mut(), 0) };
    if rc >= 0 {
        return Ok(true);
    }
    let err = io::Error::last_os_error();
    match err.raw_os_error() {
        Some(libc::ENODATA | libc::ENOTSUP) => Ok(false),
        _ => Err(err),
    }
}

/// A short, deterministic hex digest of a path, used only as a directory name.
fn path_hash(path: &Path) -> String {

View on GitHub (pinned to affd8760f4)