rust-lang / rust-lang/rust

`EnvKey::cmp` truncates string length to i32 on Windows

Open
#160,893 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-process C-bug O-windows S-has-mcve T-libs
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.