ramensoftware/windhawk · error · SettingsError
value name contains a NUL
Error message
value name {name:?} contains a NUL What it means
The registry settings backend refuses value names that contain an embedded NUL character, because the Windows registry API uses NUL-terminated wide strings for names — an embedded NUL would silently truncate the name. The check runs before key-state checks so the same name always fails identically, matching the INI backend's behavior.
Solutions
- Trim at the first NUL before using the name: name.split('\0').next().unwrap_or("").
- Sanitize or reject the name at the input boundary with an explicit validation error.
- Fix the source producing the name (e.g. convert UTF-16 with proper truncation instead of raw fixed buffers).
Example fix
// before
let name = String::from_utf16_lossy(&raw_buf); // may contain \0
settings.set_string(&name, value)?;
// after
let name = String::from_utf16_lossy(&raw_buf);
let name = name.split('\0').next().unwrap_or("");
settings.set_string(name, value)?; Defensive patterns
Strategy: validation
Validate before calling
fn assert_name_ok(name: &str) -> Result<(), String> {
if name.contains('\0') { Err("value name contains NUL".into()) } else { Ok(()) }
} Type guard
fn is_clean_name(name: &str) -> bool { !name.contains('\0') && !name.is_empty() } Try / catch
match settings.remove(&name) {
Err(e) if e.message().contains("NUL") => eprintln!("bad value name: {name:?}"),
r => r?,
} Prevention
- Always trim NULs when converting fixed-size wide buffers to String.
- Validate identifiers at API boundaries before they reach storage.
- Convert UTF-16 with explicit length rather than lossy raw-byte conversion.
- Add a lint/test that generated names contain no control characters.
When it happens
Trigger: Calling query_raw, set_raw, or remove (or any get/set/remove built on them) with a name string containing '\0', typically a name sliced from a fixed-size buffer or constructed from raw bytes.
Common situations: Building value names from C-style buffers that keep trailing/embedded NULs; interop code passing byte arrays instead of trimmed strings; migrating names from a format that allows NULs.
Understand the failure class
Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.
Related errors
- value is too large for the registry
- value contains a NUL character
- RegQueryValueEx (size)
- RegQueryValueEx (data)
- RegSetValueEx
AI-assisted analysis of ramensoftware/windhawk@61d99ed8e1 (2026-09-12).
Data as JSON: /api/errors/0f24502f23dbd3c9.
Report an issue: GitHub.
Appendix: source
Thrown at src/windhawk-core/windows/src/registry.rs:391
impl RegistryTree {
fn hkey(&self) -> Option<HKEY> {
self.key.as_ref().map(|k| k.0)
}
fn err(&self, op: &'static str, rc: u32, what: &str) -> SettingsError {
registry_err(op, self.location.clone(), rc, what)
}
/// A value name reaches Win32 as a `PCWSTR`, which ends at its first NUL, so
/// a name carrying one would read, write, or delete a DIFFERENT value - the
/// prefix - with the call reporting success. Refuse it on every name-taking
/// operation, as the INI backend refuses such a name and as `set_string`
/// refuses a NUL in the value, so the same request fails the same way in
/// both storage modes. Checked before the key state, so one name gets one
/// answer whether or not the tree is open for write.
fn check_name(&self, op: &'static str, name: &str) -> Result<(), SettingsError> {
if name.contains('\0') {
return Err(self.err(op, 0, &format!("value name {name:?} contains a NUL")));
}
Ok(())
}
/// Read a value's raw type + bytes: a size-query call (null data buffer)
/// for the type and length, then one sized read. There is no
/// `ERROR_MORE_DATA` grow loop - the size query returns the exact length,
/// so the single follow-up read always fits.
fn query_raw(&self, name: &str) -> Result<Option<(u32, Vec<u8>)>, SettingsError> {
self.check_name("get", name)?;
let Some(hkey) = self.hkey() else {
return Ok(None);
};
let name_w = to_wide(name);
let mut size: u32 = 0;
let mut value_type: u32 = 0;
// First call: type + size only.
// SAFETY: name_w is NUL-terminated; out params are valid; lpData nullView on GitHub (pinned to 61d99ed8e1)