
Loading

Loading
We use strictly necessary cookies to run this site, and analytics cookies to understand how it's used. See our Privacy Policy for details.
A GitHub-reviewed security advisory, not necessarily under active exploitation the way our CISA KEV advisories are, but a real, disclosed vulnerability in an open-source dependency.
Affected packages
Vulnerable: <= 0.2.2
Patched in 0.2.3
Details
MemoryMap::read in the pageant crate (part of the russh workspace, used by
russh's SSH-agent client on Windows via AgentClient::connect_pageant) copies a
peer-controlled number of bytes out of an 8192-byte shared-memory view with
no bounds check — unlike the sibling MemoryMap::write, which correctly
rejects oversize access with Error::Overflow. The byte count comes straight
from a u32 length prefix that the responding "Pageant" process writes into the
shared mapping. A malicious local process that answers as the Pageant agent can
therefore cause:
u32 (vec![0; n]).This was reproduced end-to-end against the real, unmodified pageant crate
(not a model) on x86_64-pc-windows-gnu under Wine; see "Proof of concept".
MemoryMap::read(size) walks off the end of
the 8192-byte view and faults on the next, unmapped page — an
EXCEPTION_ACCESS_VIOLATION that crashes the russh SSH client. Independently,
a size near u32::MAX drives a ~4 GiB vec![0; n] before any copy.read returns those adjacent bytes to russh as the
"agent response", which russh then parses as agent identities/signatures. This
arm depends on process memory layout, so it is opportunistic; the crash/alloc
is the deterministic outcome.FindWindowW("Pageant", "Pageant") and passes the shared-mapping name inside
the WM_COPYDATA COPYDATASTRUCT. Any local process can register a window
of class + title "Pageant", receive that name, open the same mapping, and
write a hostile size. So an unprivileged local process impersonating Pageant
can attack every russh-based SSH client that uses the Pageant agent.pageant/src/wmmessage.rs
MemoryMap::read (:160-171) — no bound (contrast MemoryMap::write
:139-158, which returns Error::Overflow when pos + len > length).query_pageant_direct (:199-237) — reads a 4-byte u32 size from the
shared mapping (:233) and calls map.read(size) (:234) with no check
against _AGENT_MAX_MSGLEN (8192).AgentClient::connect_pageant → PageantStream →
query_pageant_direct.Platform: Windows only (cfg(windows)), local attacker. Verified against
the pageant crate v0.2.2 as shipped in russh v0.63.1 (d3ae702, the
latest release). read has never had a bound in any revision
(git log -p -- pageant/src/wmmessage.rs).
fn write(&mut self, data: &[u8]) -> Result<(), Error> {
if self.pos + data.len() > self.length { // :140 BOUND PRESENT
return Err(Error::Overflow);
}
... copy_nonoverlapping(&data[0], view+pos, data.len()) ...
}
fn read(&mut self, n: usize) -> Vec<u8> { // :160 NO BOUND
let out = vec![0; n]; // n up to 0xFFFF_FFFF (CWE-789)
unsafe {
std::ptr::copy_nonoverlapping(
self.view.Value.add(self.pos) as *const u8, // view is length==8192
out.as_ptr() as *mut u8,
n, // reads n bytes, may run past the view (CWE-125)
);
}
self.pos += n;
out
}
query_pageant_direct creates the mapping at _AGENT_MAX_MSGLEN = 8192, writes
the request, sends the WM_COPYDATA, then reads the response the peer wrote:
map.seek(0);
let mut buf = map.read(4);
let size = u32::from_be_bytes([buf[0],buf[1],buf[2],buf[3]]) as usize; // :233 peer-controlled
buf.extend(map.read(size)); // :234 unbounded
Nothing checks 4 + size <= 8192, so map.read(size) runs past the 8 KiB view.
Because the bug is Windows-only (WM_COPYDATA + MapViewOfFile), the PoC is a
Windows cross-build (x86_64-pc-windows-gnu) driven under Wine, entirely
inside a Linux container. It exercises the real, unmodified crate: the PoC
takes a path dependency on pageant and calls
pageant::wmmessage::query_pageant_direct, exactly what
AgentClient::connect_pageant uses. A second thread impersonates Pageant
(registers the window class + title "Pageant") and, on WM_COPYDATA, opens
the shared mapping russh created and writes an attacker-chosen 4-byte
big-endian length.
poc/run.sh builds and runs it. Full log in results/e2e-wine-run.log:
======== LEG 1 — ATTACK: fake agent reports length 0x00080000 (512 KiB) >> 8192-byte view ========
[attacker] impersonating Pageant window is up (class+title "Pageant")
[victim ] calling pageant::wmmessage::query_pageant_direct() over the 8192-byte view ...
[attacker] WM_COPYDATA received; shared mapping = "PageantRequestpoc"
[attacker] wrote hostile response length = 524288 (0x00080000) into the 8192-byte view
wine: Unhandled page fault on read access to 0000000002092000 at address 00000002282CFDC4 ...
WINE-EXIT=5
======== LEG 2 — CONTROL: fake agent reports length 0x00000010 (16 B), in-bounds ========
[victim ] returned 20 bytes — request+response fit inside the 8192-byte view (in-bounds control); no fault
WINE-EXIT=0
size makes MemoryMap::read read past the
8192-byte view; Wine reports an unhandled page fault at a page-aligned address
(0x…2092000) — the out-of-bounds read as an access violation (crash). Exit
code 5 = STATUS_ACCESS_VIOLATION.size returns cleanly. Same code path; the
only difference is whether the peer's length exceeds the view — isolating the
missing bound.Fix validation. With patch/pageant-read-bound.patch applied and the PoC
rebuilt against the patched crate, the identical attack input is rejected
(results/patched-wine-run.log):
[attacker] wrote hostile response length = 524288 (0x00080000) into the 8192-byte view
[victim ] query_pageant_direct error: Overflow
WINE-EXIT=0
No fault; the read is refused at the bound, exactly as write already refuses
oversize writes. (A pure-logic Linux model of the same control flow is also
included as poc/pageant_read_oob_demo.rs / results/logic-demo-run.log.)
Mirror write's guard in read and validate the response length before
allocating/copying. patch/pageant-read-bound.patch:
MemoryMap::read(n) returns Result<Vec<u8>, Error> and returns
Error::Overflow when self.pos + n > self.length;query_pageant_direct propagates that Result and additionally rejects
size > _AGENT_MAX_MSGLEN - 4 before map.read(size).EPSS, exploit probability
Low0.19%
estimated chance of real-world exploitation in the next 30 days, higher than 8.8% of every CVE FIRST.org scores
Refreshed 11/15/2026, via FIRST.org's EPSS model, not CVSS, this measures likelihood of exploitation, not how severe it would be.
More rust advisories
Talk to us about this
Find out whether this dependency (or others like it) is actually reachable in your codebase.
Get help patching, or building dependency-scanning into your pipeline going forward.
A secure code review or supply chain review finds this before an advisory does.