Summary
pageant: Out-of-bounds read / oversized allocation in pageant MemoryMap::read via a malicious Pageant agent (Windows)
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:
- an out-of-bounds read past the 8 KiB view (access violation → process
crash; or disclosure of adjacent process memory if the following page is
committed) - an allocation of up to ~4 GiB from a single
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".
Affected component
pageant/src/wmmessage.rsMemoryMap::read(:160-171), no bound (contrastMemoryMap::write:139-158, which returnsError::Overflowwhenpos + len > length).query_pageant_direct(:199-237), reads a 4-byteu32size from the
shared mapping (:233) and callsmap.read(size)(:234) with no check
against_AGENT_MAX_MSGLEN(8192).
- Reached from russh via
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).
Details
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.
Proof of concept
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 callspageant::wmmessage::query_pageant_direct, exactly whatAgentClient::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
- LEG 1 (attack): an oversized
sizemakesMemoryMap::readread 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. - LEG 2 (control): an in-bounds
sizereturns 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.)
Impact
- Availability / DoS (reliable).
MemoryMap::read(size)walks off the end of
the 8192-byte view and faults on the next, unmapped page, anEXCEPTION_ACCESS_VIOLATIONthat crashes the russh SSH client. Independently,
asizenearu32::MAXdrives a ~4 GiBvec![0; n]before any copy. - Confidentiality (conditional). If memory immediately after the mapped view
happens to be committed,readreturns 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. - Trust boundary. russh locates the agent with
FindWindowW("Pageant", "Pageant")and passes the shared-mapping name inside
theWM_COPYDATACOPYDATASTRUCT. Any local process can register a window
of class + title"Pageant", receive that name, open the same mapping, and
write a hostilesize. So an unprivileged local process impersonating Pageant
can attack every russh-based SSH client that uses the Pageant agent.
A read operation accesses a memory location beyond the intended buffer boundary. Typical impact: sensitive data disclosure or crash.
CVE-2026-102820 has a CVSS score of 6.2 (Medium). The vector is requires local access, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (0.2.3); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
Mirror write's guard in read and validate the response length before
allocating/copying. patch/pageant-read-bound.patch:
MemoryMap::read(n)returnsResult<Vec<u8>, Error>and returnsError::Overflowwhenself.pos + n > self.length;query_pageant_directpropagates thatResultand additionally rejectssize > _AGENT_MAX_MSGLEN - 4beforemap.read(size).
Frequently Asked Questions
- What is CVE-2026-102820? CVE-2026-102820 is a medium-severity out-of-bounds read vulnerability in pageant (rust), affecting versions <= 0.2.2. It is fixed in 0.2.3. A read operation accesses a memory location beyond the intended buffer boundary.
- How severe is CVE-2026-102820? CVE-2026-102820 has a CVSS score of 6.2 (Medium). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of pageant are affected by CVE-2026-102820? pageant (rust) versions <= 0.2.2 is affected.
- Is there a fix for CVE-2026-102820? Yes. CVE-2026-102820 is fixed in 0.2.3. Upgrade to this version or later.
- Is CVE-2026-102820 exploitable, and should I be worried? Whether CVE-2026-102820 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-102820 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-102820? Upgrade
pageantto 0.2.3 or later.