ramensoftware/windhawk · error · SettingsError
value name contains a NUL
Error message
value name {name:?} contains a NUL What it means
IniTree::check_name rejects value names containing an embedded NUL character ('\0'). The read (get) and remove paths pass the name straight to Win32 APIs, where a NUL terminates the string and would silently truncate the name or address the wrong value, so it is refused up front. This keeps behavior consistent between the INI-file and registry backends: the same name gets the same answer for every operation.
Solutions
- Strip or reject the NUL before calling: use name.split('\0').next().unwrap_or("") or trim_at_nul to get the effective name.
- Fix the upstream producer so the name never contains '\0' (validate at input boundary).
- Handle the returned SettingsError by surfacing a clear validation message to the user instead of propagating it.
- If both parts of a double-NUL-terminated string are meaningful, choose the correct segment explicitly rather than passing the whole buffer.
Example fix
// before
let name = buf_as_str(); // may contain "value\0padding"
settings.get_string(name)?;
// after
let name = buf_as_str().split('\0').next().unwrap_or_default();
settings.get_string(name)?; Defensive patterns
Strategy: validation
Validate before calling
fn name_is_safe(name: &str) -> bool { !name.contains('\0') && !name.is_empty() }
if !name_is_safe(&name) { return Err(anyhow!("value name must not contain NUL")); } Type guard
fn safe_name(name: &str) -> Option<&str> {
if name.contains('\0') { None } else { Some(name) }
} Try / catch
match ini.get_string(&name) {
Err(e) if e.to_string().contains("NUL") => { /* sanitize name and retry */ },
Err(e) => return Err(e.into()),
Ok(v) => v,
} Prevention
- Trim C-style buffers at the first NUL before using them as names.
- Validate all externally-sourced names at the input boundary.
- Add a debug assertion / unit test that settings names are NUL-free.
- Never pass raw fixed-size buffers as setting names without conversion.
When it happens
Trigger: Calling get_string/get_int/get_binary/enum-related reads or remove on an IniTree with a name argument that contains '\0' (e.g. a value assembled from concatenated C-string buffers or uninitialized memory).
Common situations: Names sourced from fixed-size buffers that were not trimmed at the terminator; parsing legacy data that embeds NULs; interop code copying Windows API strings without stripping the terminator.
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
- value name
- value contains a NUL character
- GetPrivateProfileString
- value contains a line break
- Initial settings arrays must contain at least one template…
AI-assisted analysis of ramensoftware/windhawk@61d99ed8e1 (2026-09-12).
Data as JSON: /api/errors/268e5a41efe3ac6d.
Report an issue: GitHub.
Appendix: source
Thrown at src/windhawk-core/windows/src/ini.rs:164
struct IniTree {
file: PathBuf,
section: String,
}
impl IniTree {
fn err(&self, op: &'static str, os: u32, what: &str) -> SettingsError {
ini_err(op, self.file.display().to_string(), os, what)
}
/// A value name reaches the profile API as a `PCWSTR`, which ends at its
/// first NUL, so a name carrying one addresses a DIFFERENT value - the
/// prefix - with the call reporting success. The read and remove paths take
/// a name straight to Win32, so they guard it here; the write path refuses
/// the same name as one [`unrepresentable_name`] rejects, so one name gets
/// one answer whichever operation carries it, in both storage modes.
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(())
}
}
impl SettingsTree for IniTree {
fn get_string(&self, name: &str) -> Result<Option<String>, SettingsError> {
self.check_name("get", name)?;
get_profile_string(&self.file, &self.section, name)
.map_err(|os| self.err("get", os, "GetPrivateProfileString"))
}
fn set_string(&mut self, name: &str, value: &str) -> Result<(), SettingsError> {
if let Some(why) = unrepresentable_name(name) {
return Err(self.err("set", 0, &format!("value name {name:?} {why}")));
}
// `WritePrivateProfileStringW` takes the value as a NUL-terminated
// string, so an embedded NUL ends it: everything after it is dropped andView on GitHub (pinned to 61d99ed8e1)