{"record":{"id":"268e5a41efe3ac6d","repo":"ramensoftware/windhawk","slug":"value-name-name-contains-a-nul","errorCode":null,"errorMessage":"value name {name:?} contains a NUL","messagePattern":"value name (.+?) contains a NUL","errorType":"validation","errorClass":"SettingsError","httpStatus":null,"severity":"error","filePath":"src/windhawk-core/windows/src/ini.rs","lineNumber":164,"sourceCode":"struct IniTree {\n    file: PathBuf,\n    section: String,\n}\n\nimpl IniTree {\n    fn err(&self, op: &'static str, os: u32, what: &str) -> SettingsError {\n        ini_err(op, self.file.display().to_string(), os, what)\n    }\n\n    /// A value name reaches the profile API as a `PCWSTR`, which ends at its\n    /// first NUL, so a name carrying one addresses a DIFFERENT value - the\n    /// prefix - with the call reporting success. The read and remove paths take\n    /// a name straight to Win32, so they guard it here; the write path refuses\n    /// the same name as one [`unrepresentable_name`] rejects, so one name gets\n    /// one answer whichever operation carries it, in both storage modes.\n    fn check_name(&self, op: &'static str, name: &str) -> Result<(), SettingsError> {\n        if name.contains('\\0') {\n            return Err(self.err(op, 0, &format!(\"value name {name:?} contains a NUL\")));\n        }\n        Ok(())\n    }\n}\n\nimpl SettingsTree for IniTree {\n    fn get_string(&self, name: &str) -> Result<Option<String>, SettingsError> {\n        self.check_name(\"get\", name)?;\n        get_profile_string(&self.file, &self.section, name)\n            .map_err(|os| self.err(\"get\", os, \"GetPrivateProfileString\"))\n    }\n\n    fn set_string(&mut self, name: &str, value: &str) -> Result<(), SettingsError> {\n        if let Some(why) = unrepresentable_name(name) {\n            return Err(self.err(\"set\", 0, &format!(\"value name {name:?} {why}\")));\n        }\n        // `WritePrivateProfileStringW` takes the value as a NUL-terminated\n        // string, so an embedded NUL ends it: everything after it is dropped and","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/ramensoftware/windhawk/blob/61d99ed8e182e1af1b60109612b6763ad1b4b74e/src/windhawk-core/windows/src/ini.rs#L146-L182","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nlet name = buf_as_str(); // may contain \"value\\0padding\"\nsettings.get_string(name)?;\n\n// after\nlet name = buf_as_str().split('\\0').next().unwrap_or_default();\nsettings.get_string(name)?;","handlingStrategy":"validation","validationCode":"fn name_is_safe(name: &str) -> bool { !name.contains('\\0') && !name.is_empty() }\nif !name_is_safe(&name) { return Err(anyhow!(\"value name must not contain NUL\")); }","typeGuard":"fn safe_name(name: &str) -> Option<&str> {\n    if name.contains('\\0') { None } else { Some(name) }\n}","tryCatchPattern":"match ini.get_string(&name) {\n    Err(e) if e.to_string().contains(\"NUL\") => { /* sanitize name and retry */ },\n    Err(e) => return Err(e.into()),\n    Ok(v) => v,\n}","preventionTips":["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."],"tags":["ini","nul-character","validation","windows"],"backgroundTag":"invalid-argument-value","analyzedSha":"61d99ed8e182e1af1b60109612b6763ad1b4b74e","analyzedAt":"2026-09-12T14:02:41.115Z","contentChangedAt":"2026-09-12T14:02:41.115Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}