astrid-runtime/astrid · error

xattr name has NUL

Error message

xattr name has NUL

What it means

has_xattr probes an extended attribute via libc::lgetxattr, which requires NUL-terminated C strings. The xattr name passed as a Rust &str contained an embedded NUL byte, so it cannot be converted to a CString; the function fails closed with InvalidInput rather than calling the FFI with a truncated name.

Solutions

  1. Verify the xattr name passed to has_xattr contains no interior NUL bytes before the call
  2. Use only static, trusted xattr name constants (e.g. 'trusted.overlay.opaque') instead of dynamically built names
  3. If names come from external data, sanitize/reject any byte string containing 0x00 before use
  4. Log the offending name (hex-escaped) to identify where the NUL originates

Example fix

// before
let name = format!("trusted.overlay.{}", user_blob);
has_xattr(path, &name)?;
// after
if name.bytes().any(|b| b == 0) { return Err(...); }
has_xattr(path, "trusted.overlay.opaque")?;
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_no_nul(s: &str) -> io::Result<()> { if s.bytes().any(|b| b == 0) { Err(io::Error::new(io::ErrorKind::InvalidInput, "xattr name contains NUL")) } else { Ok(()) } }

Type guard

fn is_nul_free(s: &str) -> bool { !s.as_bytes().contains(&0) }

Try / catch

match has_xattr(path, name) { Err(e) if e.kind() == io::ErrorKind::InvalidInput => { /* skip/reject this xattr name */ }, Err(e) => return Err(e), Ok(has) => /* proceed */, }

Prevention

When it happens

Trigger: Calling ensure_no_opaque_markers (which calls has_xattr) on overlayfs directories when a derived xattr name contains an interior '\0' — e.g. a name built from untrusted or corrupted metadata rather than a trusted constant like 'trusted.overlay.opaque'.

Common situations: Corrupted or attacker-influenced overlay metadata producing xattr names with embedded NULs; a bug in code that assembles xattr names from byte buffers instead of static 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/408b088034f0e286. Report an issue: GitHub.

Appendix: source

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

                     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 {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};

View on GitHub (pinned to affd8760f4)