diff options
| author | JJ Liebig <jonathan.liebig@gmail.com> | 2026-09-01 23:14:22 +0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-01 21:14:22 +0200 |
| commit | 5616196942cbe752cc0659b9bd0fb616b2a6ed5c (patch) | |
| tree | 89ffcbfe715498265bc6728e5450b66668385121 /vendor/patches | |
| parent | e2b85c73615b37a483eefa839923d9aff8e629b3 (diff) | |
fix(windows): reject malformed process environments (#3500)
* fix(windows): reject malformed process environments
refs #3430
* fix(windows): validate registry environment strings
refs #3430
* fix(windows): preserve hidden environment variables
refs #3430
Diffstat (limited to 'vendor/patches')
| -rw-r--r-- | vendor/patches/portable-pty/0003-reject-malformed-windows-environments.patch | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/vendor/patches/portable-pty/0003-reject-malformed-windows-environments.patch b/vendor/patches/portable-pty/0003-reject-malformed-windows-environments.patch new file mode 100644 index 00000000..d2f7c4ae --- /dev/null +++ b/vendor/patches/portable-pty/0003-reject-malformed-windows-environments.patch @@ -0,0 +1,220 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: herdr maintainers <maintainers@herdr.dev> +Date: Tue, 1 Sep 2026 00:00:00 +0000 +Subject: [PATCH] reject malformed Windows process environments + +Windows registry environment keys can contain empty names, non-string types, +or malformed UTF-16. Decode string values into aligned storage, check Win32 +expansion failures, preserve valid drive variables, and omit entries that +cannot form a valid environment record before calling CreateProcessW. + +Herdr issue: https://github.com/herdrdev/herdr/issues/3430 +Vendored base: portable-pty 0.9.0 +--- +diff --git a/vendor/portable-pty/src/cmdbuilder.rs b/vendor/portable-pty/src/cmdbuilder.rs +index 385fe81c..b214bb6d 100644 +--- a/vendor/portable-pty/src/cmdbuilder.rs ++++ b/vendor/portable-pty/src/cmdbuilder.rs +@@ -71,6 +71,70 @@ fn get_shell() -> String { + "/bin/sh".into() + } + ++#[cfg(windows)] ++fn reg_value_to_string(value: &winreg::RegValue) -> anyhow::Result<OsString> { ++ use std::os::windows::ffi::OsStringExt; ++ use winapi::um::processenv::ExpandEnvironmentStringsW; ++ use winreg::enums::RegType; ++ ++ if !matches!(value.vtype, RegType::REG_SZ | RegType::REG_EXPAND_SZ) { ++ anyhow::bail!( ++ "unsupported registry type {:?} for environment variable", ++ value.vtype ++ ); ++ } ++ ++ let mut bytes = value.bytes.chunks_exact(2); ++ let mut units: Vec<u16> = bytes ++ .by_ref() ++ .map(|pair| u16::from_le_bytes([pair[0], pair[1]])) ++ .collect(); ++ anyhow::ensure!( ++ bytes.remainder().is_empty(), ++ "invalid UTF-16 byte length for environment variable" ++ ); ++ while let Some(0) = units.last() { ++ units.pop(); ++ } ++ anyhow::ensure!( ++ !units.contains(&0), ++ "embedded null in registry environment variable" ++ ); ++ ++ if value.vtype == RegType::REG_SZ { ++ return Ok(OsString::from_wide(&units)); ++ } ++ ++ units.push(0); ++ let size = unsafe { ExpandEnvironmentStringsW(units.as_ptr(), std::ptr::null_mut(), 0) }; ++ if size == 0 { ++ anyhow::bail!( ++ "ExpandEnvironmentStringsW failed: {}", ++ std::io::Error::last_os_error() ++ ); ++ } ++ let mut buf = vec![0u16; size as usize]; ++ let written = unsafe { ++ ExpandEnvironmentStringsW(units.as_ptr(), buf.as_mut_ptr(), buf.len() as u32) ++ }; ++ if written == 0 { ++ anyhow::bail!( ++ "ExpandEnvironmentStringsW failed: {}", ++ std::io::Error::last_os_error() ++ ); ++ } ++ anyhow::ensure!( ++ written as usize <= buf.len(), ++ "environment changed while expanding registry value" ++ ); ++ ++ buf.truncate(written as usize); ++ while let Some(0) = buf.last() { ++ buf.pop(); ++ } ++ Ok(OsString::from_wide(&buf)) ++} ++ + fn get_base_env() -> BTreeMap<OsString, EnvEntry> { + let mut env: BTreeMap<OsString, EnvEntry> = std::env::vars_os() + .map(|(key, value)| { +@@ -103,37 +167,8 @@ fn get_base_env() -> BTreeMap<OsString, EnvEntry> { + + #[cfg(windows)] + { +- use std::os::windows::ffi::OsStringExt; +- use winapi::um::processenv::ExpandEnvironmentStringsW; +- use winreg::enums::{RegType, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE}; +- use winreg::types::FromRegValue; +- use winreg::{RegKey, RegValue}; +- +- fn reg_value_to_string(value: &RegValue) -> anyhow::Result<OsString> { +- match value.vtype { +- RegType::REG_EXPAND_SZ => { +- let src = unsafe { +- std::slice::from_raw_parts( +- value.bytes.as_ptr() as *const u16, +- value.bytes.len() / 2, +- ) +- }; +- let size = +- unsafe { ExpandEnvironmentStringsW(src.as_ptr(), std::ptr::null_mut(), 0) }; +- let mut buf = vec![0u16; size as usize + 1]; +- unsafe { +- ExpandEnvironmentStringsW(src.as_ptr(), buf.as_mut_ptr(), buf.len() as u32) +- }; +- +- let mut buf = buf.as_slice(); +- while let Some(0) = buf.last() { +- buf = &buf[0..buf.len() - 1]; +- } +- Ok(OsString::from_wide(buf)) +- } +- _ => Ok(OsString::from_reg_value(value)?), +- } +- } ++ use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE}; ++ use winreg::RegKey; + + if let Ok(sys_env) = RegKey::predef(HKEY_LOCAL_MACHINE) + .open_subkey("System\\CurrentControlSet\\Control\\Session Manager\\Environment") +@@ -662,12 +697,24 @@ impl CommandBuilder { + value, + } in self.envs.values() + { +- block.extend(preferred_key.encode_wide()); ++ let key: Vec<u16> = preferred_key.encode_wide().collect(); ++ if key.is_empty() ++ || key.contains(&0) ++ || key[1..].contains(&(b'=' as u16)) ++ || value.encode_wide().any(|unit| unit == 0) ++ { ++ continue; ++ } ++ ++ block.extend(key); + block.push(b'=' as u16); + block.extend(value.encode_wide()); + block.push(0); + } + // and a final terminator for CreateProcessW ++ if block.is_empty() { ++ block.push(0); ++ } + block.push(0); + + block +@@ -869,4 +916,65 @@ mod tests { + assert!(command.ends_with(r#"/d /c echo "hi""#), "{}", command); + assert!(!command.contains(r#"\"hi\""#), "{}", command); + } ++ ++ #[cfg(windows)] ++ #[test] ++ fn windows_environment_rejects_malformed_entries() { ++ use std::os::windows::ffi::{OsStrExt, OsStringExt}; ++ use winreg::enums::RegType; ++ use winreg::RegValue; ++ ++ let multi_string = RegValue { ++ bytes: vec![b'a', 0, 0, 0], ++ vtype: RegType::REG_MULTI_SZ, ++ }; ++ assert!(reg_value_to_string(&multi_string).is_err()); ++ let malformed_expand_string = RegValue { ++ bytes: vec![b'%', 0, b'P'], ++ vtype: RegType::REG_EXPAND_SZ, ++ }; ++ assert!(reg_value_to_string(&malformed_expand_string).is_err()); ++ let expandable_string = RegValue { ++ bytes: "literal\0" ++ .encode_utf16() ++ .flat_map(u16::to_le_bytes) ++ .collect(), ++ vtype: RegType::REG_EXPAND_SZ, ++ }; ++ assert_eq!( ++ reg_value_to_string(&expandable_string).unwrap(), ++ OsString::from("literal") ++ ); ++ ++ let mut cmd = CommandBuilder::new("dummy"); ++ cmd.env_clear(); ++ cmd.env("", "ignored"); ++ cmd.env( ++ OsString::from_wide(&[b'B' as u16, 0, b'A' as u16, b'D' as u16]), ++ "ignored", ++ ); ++ cmd.env( ++ "MULTI", ++ OsString::from_wide(&[b'a' as u16, 0, b'b' as u16]), ++ ); ++ cmd.env("BAD=KEY", "ignored"); ++ cmd.env("=::", "colon"); ++ cmd.env("=C:", r"C:\valid"); ++ cmd.env("=ExitCode", "hidden"); ++ cmd.env("EMPTY", ""); ++ cmd.env("VALID", "ok"); ++ ++ let expected: Vec<u16> = ++ OsStr::new("=::=colon\0=C:=C:\\valid\0=ExitCode=hidden\0EMPTY=\0VALID=ok\0\0") ++ .encode_wide() ++ .collect(); ++ assert_eq!(cmd.environment_block(), expected); ++ ++ cmd.env_remove("=::"); ++ cmd.env_remove("=C:"); ++ cmd.env_remove("=ExitCode"); ++ cmd.env_remove("EMPTY"); ++ cmd.env_remove("VALID"); ++ assert_eq!(cmd.environment_block(), vec![0, 0]); ++ } + } |
