`EnvKey::cmp` truncates string length to i32 on Windows
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
There is a bug in Windows implementation of EnvKey::cmp that the key's length is truncated to i32 before comparison.
Command::env call goes through CommandEnv, which internally uses BTreeMap<EnvKey, Option<OsString>> to keep track of environment variables.
https://github.com/rust-lang/rust/blob/fdda4c6a308e5ae5514757601fd41b2268665ce7/library/std/src/process.rs#L832-L839
https://github.com/rust-lang/rust/blob/fdda4c6a308e5ae5514757601fd41b2268665ce7/library/std/src/sys/process/env.rs#L6-L12
https://github.com/rust-lang/rust/blob/fdda4c6a308e5ae5514757601fd41b2268665ce7/library/std/src/sys/process/env.rs#L49-L54
The system-specific key comparison is handled by EnvKey struct. On Windows, the WTF-8 OsString is converted to UTF-16 and compared using the CompareStringOrdinal C API.
https://github.com/rust-lang/rust/blob/fdda4c6a308e5ae5514757601fd41b2268665ce7/library/std/src/sys/process/windows.rs#L123-L129
https://github.com/rust-lang/rust/blob/fdda4c6a308e5ae5514757601fd41b2268665ce7/library/std/src/sys/process/windows.rs#L37-L46
https://github.com/rust-lang/rust/blob/fdda4c6a308e5ae5514757601fd41b2268665ce7/library/std/src/sys/process/windows.rs#L74-L93
https://github.com/rust-lang/rust/blob/fdda4c6a308e5ae5514757601fd41b2268665ce7/library/std/src/sys/pal/windows/c/windows_sys.rs#L10
Here, self.utf16.len() as _ truncates usize buffer length to i32 before calling CompareStringOrdinal which may result in incorrect comparison.
PoC
This PoC code demonstrates a logic bug where two different keys collide due to the length truncation, leaving only one key to the env map despite two .env calls. (only works on Windows)
use std::ffi::OsString;
use std::process::Command;
fn main() {
const OVERFLOW_TO_ZERO: usize = 0x1_0000_0000;
println!("Allocating the first huge string");
let huge1 = OsString::from(String::from("0000") + &"A".repeat(OVERFLOW_TO_ZERO));
println!("Allocating the second huge string");
let huge2 = OsString::from(String::from("0000") + &"B".repeat(OVERFLOW_TO_ZERO));
assert_eq!(huge1.len() as i32, 4);
assert_eq!(huge2.len() as i32, 4);
// Although huge1 and huge2 are not equal, they land to the same key in the internal env BTreeMap
// because only the first 4 bytes are used for comparison.
assert_ne!(&huge1, &huge2);
let mut command = Command::new("cmd");
command
.env(huge1, "value1")
.env(huge2, "value2");
// BUG: only shows a single key in the output. Truncated to the first 10 characters for readability.
dbg!(command.get_envs().map(|(k, v)| (&k.to_str().unwrap()[..10], v)).collect::<Vec<_>>());
}
Output:
Allocating the first huge string
Allocating the second huge string
[src\main.rs:26:5] command.get_envs().map(|(k, v)|
(&k.to_str().unwrap()[..10], v)).collect::<Vec<_>>() = [
(
"0000AAAAAA",
Some(
"value2",
),
),
]
> rustc --version --verbose
rustc 1.97.1 (8bab26f4f 2026-07-14)
binary: rustc
commit-hash: 8bab26f4f68e0e26f0bb7960be334d5b520ea452
commit-date: 2026-07-14
host: x86_64-pc-windows-msvc
release: 1.97.1
LLVM version: 22.1.6
Investigation on potential buffer over-read with -1 overflow
CompareStringOrdinal accepts -1 as a valid length value. In that case, the passed string is considered null-terminated, and CompareStringOrdinal scans the string until the null value is found.
I suspected CompareStringOrdinal may read out of bounds if non-zero bytes continues after the allocated string when the length parameter overflows to -1. After the investigation, I concluded that this OOB read path doesn't lead to security issues.
To make the length parameter passed to CompareStringOrdinal -1, k.encode_wide().collect() needs an allocation of at least 0xffff_ffff * 2 bytes of memory. An allocation this huge always went through HeapAlloc -> VirtualAlloc path with MEM_COMMIT flag in my experiment, which zeroes the page on the first access, guaranteeing that there is a null byte right after the buffer. This might still be a single-null-byte OOB read, but I couldn't find any evidences that the bug leads to Rust-side memory safety issues.
The initial discovery was made by MDASH, an agentic code scanner. All technical claims are reviewed and revised by human experts.
No LLM was used to write this issue report following the Rust's LLM usage policy.
Reporting on behalf of Microsoft FORGE Lab.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with EnvKey and the Windows comparison code in library/std/src/sys/process/windows.rs, then trace how it is used by CommandEnv in library/std/src/sys/process/env.rs. Verify the comparison preserves the full OsString length and that distinct oversized environment keys remain distinct in the BTreeMap, using the Windows-only PoC behavior as the regression target.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- operating-systems
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 64/100