ramensoftware/windhawk · error · SettingsError

value contains a NUL character

Error message

value contains a NUL character

What it means

set_string refuses string values containing an embedded NUL, because REG_SZ is defined up to its first NUL: everything after it would be silently lost on read-back by this library, the C++ engine, and regedit alike. Refusing the write keeps INI and registry backends failing the same way instead of silently truncating.

Solutions

  1. Trim at the first NUL before storing: value.split('\0').next().unwrap_or("").
  2. Sanitize the input at the boundary (reject or strip NULs with an explicit validation error).
  3. If binary data with NULs must be stored, use set_binary (hex-encoded) instead of set_string.
  4. Fix the producer of the string to not include terminators (e.g. convert UTF-16 with correct length).

Example fix

// before
let s = String::from_utf16_lossy(&wbuf); // may embed \0
settings.set_string("val", &s)?; // Err
// after
let s = String::from_utf16_lossy(&wbuf);
let s = s.split('\0').next().unwrap_or("").to_string();
settings.set_string("val", &s)?;
Defensive patterns

Strategy: validation

Validate before calling

fn assert_no_nul(v: &str) -> Result<(), String> {
    if v.contains('\0') { Err("value contains NUL".into()) } else { Ok(()) }
}

Type guard

fn is_nul_free(v: &str) -> bool { !v.contains('\0') }

Try / catch

match settings.set_string("k", &v) {
    Err(e) if e.message().contains("NUL") => eprintln!("strip NULs before storing"),
    r => r?,
}

Prevention

When it happens

Trigger: Calling set_string with a string containing '\0', usually from wide-string/UTF-16 conversions, fixed-size buffers converted to String without trimming, or concatenating raw bytes into a string.

Common situations: Interop code turning WCHAR buffers (with padding NULs) into Strings; values assembled from C strings with terminator included; decrypting/decompressing data that yields embedded NULs.

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


AI-assisted analysis of ramensoftware/windhawk@61d99ed8e1 (2026-09-12). Data as JSON: /api/errors/4ceb59e113cbde1c. Report an issue: GitHub.

Appendix: source

Thrown at src/windhawk-core/windows/src/registry.rs:503

    }
}

impl SettingsTree for RegistryTree {
    fn get_string(&self, name: &str) -> Result<Option<String>, SettingsError> {
        Ok(match self.query_raw(name)? {
            Some((t, bytes)) if t == REG_SZ => Some(decode_sz(&bytes)),
            _ => None,
        })
    }

    fn set_string(&mut self, name: &str, value: &str) -> Result<(), SettingsError> {
        // A REG_SZ is defined up to its first NUL, so a value carrying an
        // embedded one reads back truncated - here (`decode_sz`), in the C++
        // engine, and in regedit alike. Refuse it, as the INI backend does, so
        // the same write fails the same way in both storage modes instead of
        // losing the tail in one of them.
        if value.contains('\0') {
            return Err(self.err("set", 0, "value contains a NUL character"));
        }
        // REG_SZ includes the terminating NUL, matching the C++
        // (wcslen+1)*sizeof(WCHAR) write.
        let mut wide = to_wide(value);
        let bytes: Vec<u8> = std::mem::take(&mut wide)
            .into_iter()
            .flat_map(u16::to_le_bytes)
            .collect();
        self.set_raw(name, REG_SZ, &bytes)
    }

    fn get_int(&self, name: &str) -> Result<Option<i32>, SettingsError> {
        Ok(match self.query_raw(name)? {
            Some((t, bytes)) if t == REG_DWORD => decode_dword(&bytes),
            _ => None,
        })
    }

View on GitHub (pinned to 61d99ed8e1)