{"record":{"id":"0f24502f23dbd3c9","repo":"ramensoftware/windhawk","slug":"value-name-name-contains-a-nul-registry","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/registry.rs","lineNumber":391,"sourceCode":"impl RegistryTree {\n    fn hkey(&self) -> Option<HKEY> {\n        self.key.as_ref().map(|k| k.0)\n    }\n\n    fn err(&self, op: &'static str, rc: u32, what: &str) -> SettingsError {\n        registry_err(op, self.location.clone(), rc, what)\n    }\n\n    /// A value name reaches Win32 as a `PCWSTR`, which ends at its first NUL, so\n    /// a name carrying one would read, write, or delete a DIFFERENT value - the\n    /// prefix - with the call reporting success. Refuse it on every name-taking\n    /// operation, as the INI backend refuses such a name and as `set_string`\n    /// refuses a NUL in the value, so the same request fails the same way in\n    /// both storage modes. Checked before the key state, so one name gets one\n    /// answer whether or not the tree is open for write.\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    /// Read a value's raw type + bytes: a size-query call (null data buffer)\n    /// for the type and length, then one sized read. There is no\n    /// `ERROR_MORE_DATA` grow loop - the size query returns the exact length,\n    /// so the single follow-up read always fits.\n    fn query_raw(&self, name: &str) -> Result<Option<(u32, Vec<u8>)>, SettingsError> {\n        self.check_name(\"get\", name)?;\n        let Some(hkey) = self.hkey() else {\n            return Ok(None);\n        };\n        let name_w = to_wide(name);\n        let mut size: u32 = 0;\n        let mut value_type: u32 = 0;\n        // First call: type + size only.\n        // SAFETY: name_w is NUL-terminated; out params are valid; lpData null","sourceCodeStart":373,"sourceCodeEnd":409,"githubUrl":"https://github.com/ramensoftware/windhawk/blob/61d99ed8e182e1af1b60109612b6763ad1b4b74e/src/windhawk-core/windows/src/registry.rs#L373-L409","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nlet name = String::from_utf16_lossy(&raw_buf); // may contain \\0\nsettings.set_string(&name, value)?;\n// after\nlet name = String::from_utf16_lossy(&raw_buf);\nlet name = name.split('\\0').next().unwrap_or(\"\");\nsettings.set_string(name, value)?;","handlingStrategy":"validation","validationCode":"fn assert_name_ok(name: &str) -> Result<(), String> {\n    if name.contains('\\0') { Err(\"value name contains NUL\".into()) } else { Ok(()) }\n}","typeGuard":"fn is_clean_name(name: &str) -> bool { !name.contains('\\0') && !name.is_empty() }","tryCatchPattern":"match settings.remove(&name) {\n    Err(e) if e.message().contains(\"NUL\") => eprintln!(\"bad value name: {name:?}\"),\n    r => r?,\n}","preventionTips":["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."],"tags":["registry","validation","nul","win32"],"backgroundTag":"invalid-identifier-format","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"}