xai-org/grok-build · error · io::Error

path contains NUL

Error message

path contains NUL

What it means

windows_extended_path converts a path to a Windows extended-length (\\?\ or \\?\UNC\) wide-string path for Win32 APIs that take UTF-16 paths. Windows cannot accept embedded NUL characters in path strings, so if the encoded wide string contains a 0 code unit the function refuses to build the extended path and throws io::ErrorKind::InvalidInput with 'path contains NUL'.

Source

Thrown at crates/codegen/xai-grok-tools/src/persistence.rs:380

                PCWSTR(from.as_ptr()),
                PCWSTR(to.as_ptr()),
                Self::WINDOWS_MOVE_FLAGS,
            )
        }
        .map_err(io::Error::other)
    }

    #[cfg(windows)]
    const WINDOWS_MOVE_FLAGS: windows::Win32::Storage::FileSystem::MOVE_FILE_FLAGS =
        windows::Win32::Storage::FileSystem::MOVE_FILE_FLAGS(1 | 8);

    #[cfg(windows)]
    fn windows_extended_path(path: &Path) -> io::Result<Vec<u16>> {
        use std::os::windows::ffi::OsStrExt;
        let path = std::path::absolute(path)?;
        let mut wide = path.as_os_str().encode_wide().collect::<Vec<_>>();
        if wide.contains(&0) {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "path contains NUL",
            ));
        }
        let unc = wide.starts_with(&[92, 92]);
        let mut result = if unc { r"\\?\UNC\" } else { r"\\?\" }
            .encode_utf16()
            .collect::<Vec<_>>();
        if unc {
            wide.drain(..2);
        }
        result.extend(wide);
        result.push(0);
        Ok(result)
    }
}

// Old `PersistenceLayer` / `PersistenceRunner` deleted.

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Trim or strip NUL bytes from the path string before constructing the PathBuf (e.g. split at the first '\0').
  2. Validate the path with path.as_os_str().to_string_lossy().contains('\0') before calling the API and sanitize it.
  3. Log the offending path bytes to find where the NUL is being introduced upstream.

Example fix

// before
let path = PathBuf::from(c_string_buf); // may contain '\0'
// after
let cleaned: &str = c_string_buf.split('\0').next().unwrap();
let path = PathBuf::from(cleaned);
Defensive patterns

Strategy: validation

Validate before calling

fn path_has_nul(p: &std::path::Path) -> bool {
    #[cfg(windows)]
    { use std::os::windows::ffi::OsStrExt; p.as_os_str().encode_wide().any(|c| c == 0) }
    #[cfg(not(windows))]
    { false }
}
// if path_has_nul(&p) { sanitize before calling }

Type guard

fn is_safe_path(p: &std::path::Path) -> bool {
    p.as_os_str().to_str().map(|s| !s.contains('\0')).unwrap_or(false)
}

Try / catch

match persistence::save(&path) {
    Ok(v) => v,
    Err(e) if e.kind() == std::io::ErrorKind::InvalidInput && e.to_string().contains("NUL") => {
        let clean = sanitize_nul(&path);
        persistence::save(&clean)?
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: Calling any persistence API on Windows whose path argument (after std::path::absolute) encodes to a wide string containing a NUL code unit — practically only when a Rust OsStr was constructed from raw bytes containing interior NULs, or a path was built from a buffer/string that includes '\0'.

Common situations: Paths read from binary data, fixed-size C buffers, or legacy configs that include a trailing or interior NUL byte; converting C FFI strings to PathBuf without trimming the terminator.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/a82368f7341a0c05. Report an issue: GitHub.