CVE-2026-102820: pageant Out-of-bounds Read / Oversized Allocation Vulnerability

MODERATE
Advisory Identifier: GHSA-g4mp-vgx3-xrvm
CVSS 6.2
CWE-125 CWE-789
Published
Advisory Summary

CVE-2026-102820 is a MODERATE (CVSS 6.2) out-of-bounds read / oversized allocation vulnerability (CWE-125: Out-of-bounds Read) affecting pageant libraries in the crates.io ecosystem. The issue involves out-of-bounds read / oversized allocation in `pageant` memorymap::read via a malicious pageant agent (windows). Remediation is available by upgrading affected packages to version 0.2.3.

CVE-2026-102820 at a Glance

CVE Identifier CVE-2026-102820
GHSA Identifier GHSA-g4mp-vgx3-xrvm
Severity MODERATE
CVSS v3.1 Base Score 6.2
CWE Classification CWE-125: Out-of-bounds Read
Affected Product pageant
Package Ecosystem crates.io
Published Date
Last Updated

Security Impact

CVSS: 6.2

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).

Attack Vector Local (AV:L)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (A:H)

How to Fix CVE-2026-102820

To remediate this vulnerability, upgrade all affected pageant dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

pageant fixed: 0.2.3
Upgrade pageant to 0.2.3
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for pageant

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-102820, inspect your project dependencies for pageant:

inspect dependencies for pageant

Analyze the terminal output to locate any version of pageant in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.2.3), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-125: Out-of-bounds Read, where the product reads data past the end, or before the beginning, of the intended buffer. In pageant, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary `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". ## Impact - **Availability / DoS (reliable).** `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. - **Confidentiality (conditional).** If memory immediately after the mapped view happens to be committed, `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. - **Trust boundary.** russh locates the agent with `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. ## Affected component - `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). - 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 ```rust 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: ```rust 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 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 ``` - **LEG 1 (attack)**: an oversized `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`. - **LEG 2 (control)**: an in-bounds `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`.) ## Remediation 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)`.

CVE-2026-102820 Affected pageant Versions

1 component(s)
Fixed Versions:
0.2.3
Product / Package Ecosystem Affected Versions Fixed Version
pageant
crates.io
< 0.2.3
0.2.3

References

Frequently Asked Questions

What is CVE-2026-102820?
CVE-2026-102820 is a security vulnerability in pageant involving Out-of-bounds read / oversized allocation in `pageant` MemoryMap::read via a malicious Pageant agent (Windows) under CWE-125: Out-of-bounds Read.
Which pageant versions are affected?
Affected versions include releases prior to 0.2.3 across affected packages: pageant.
What version fixes CVE-2026-102820?
Upgrading to 0.2.3 (or the latest patched release for your respective branch) resolves the vulnerability.
Is pageant affected?
Yes, pageant is affected in releases prior to 0.2.3. Upgrade your dependency to 0.2.3 or later.
What is the CVSS score of CVE-2026-102820?
CVE-2026-102820 has a CVSS v3.1 base score of 6.2 (MODERATE) with vector string CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.
What CWE is associated with CVE-2026-102820?
CVE-2026-102820 is classified under CWE-125: Out-of-bounds Read (The product reads data past the end, or before the beginning, of the intended buffer.).
How can I check crates.io dependencies for CVE-2026-102820?
Run the dependency tree command: 'inspect dependencies for pageant'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-g4mp-vgx3-xrvm",
  "modified" : "2026-09-30T23:41:03Z",
  "published" : "2026-09-30T23:41:02Z",
  "aliases" : [ "CVE-2026-102820" ],
  "summary" : "pageant: Out-of-bounds read / oversized allocation in `pageant` MemoryMap::read via a malicious Pageant agent (Windows)",
  "details" : "## Summary\n\n`MemoryMap::read` in the `pageant` crate (part of the russh workspace, used by\nrussh's SSH-agent client on Windows via `AgentClient::connect_pageant`) copies a\n**peer-controlled** number of bytes out of an 8192-byte shared-memory view with\n**no bounds check** — unlike the sibling `MemoryMap::write`, which correctly\nrejects oversize access with `Error::Overflow`. The byte count comes straight\nfrom a `u32` length prefix that the responding \"Pageant\" process writes into the\nshared mapping. A malicious local process that answers as the Pageant agent can\ntherefore cause:\n\n- an **out-of-bounds read** past the 8 KiB view (access violation → process\n  crash; or disclosure of adjacent process memory if the following page is\n  committed)\n- an allocation of up to **~4 GiB** from a single `u32` (`vec![0; n]`).\n\nThis was reproduced **end-to-end against the real, unmodified `pageant` crate**\n(not a model) on `x86_64-pc-windows-gnu` under Wine; see \"Proof of concept\".\n\n## Impact\n\n- **Availability / DoS (reliable).** `MemoryMap::read(size)` walks off the end of\n  the 8192-byte view and faults on the next, unmapped page — an\n  `EXCEPTION_ACCESS_VIOLATION` that crashes the russh SSH client. Independently,\n  a `size` near `u32::MAX` drives a ~4 GiB `vec![0; n]` before any copy.\n- **Confidentiality (conditional).** If memory immediately after the mapped view\n  happens to be committed, `read` returns those adjacent bytes to russh as the\n  \"agent response\", which russh then parses as agent identities/signatures. This\n  arm depends on process memory layout, so it is opportunistic; the crash/alloc\n  is the deterministic outcome.\n- **Trust boundary.** russh locates the agent with\n  `FindWindowW(\"Pageant\", \"Pageant\")` and passes the shared-mapping name inside\n  the `WM_COPYDATA` `COPYDATASTRUCT`. **Any** local process can register a window\n  of class + title `\"Pageant\"`, receive that name, open the same mapping, and\n  write a hostile `size`. So an unprivileged local process impersonating Pageant\n  can attack every russh-based SSH client that uses the Pageant agent.\n\n## Affected component\n\n- `pageant/src/wmmessage.rs`\n  - `MemoryMap::read` (`:160-171`) — no bound (contrast `MemoryMap::write`\n    `:139-158`, which returns `Error::Overflow` when `pos + len > length`).\n  - `query_pageant_direct` (`:199-237`) — reads a 4-byte `u32` size from the\n    shared mapping (`:233`) and calls `map.read(size)` (`:234`) with no check\n    against `_AGENT_MAX_MSGLEN` (8192).\n- Reached from russh via `AgentClient::connect_pageant` → `PageantStream` →\n  `query_pageant_direct`.\n\nPlatform: **Windows only** (`cfg(windows)`), local attacker. Verified against\nthe `pageant` crate **v0.2.2** as shipped in russh **v0.63.1** (`d3ae702`, the\nlatest release). `read` has never had a bound in any revision\n(`git log -p -- pageant/src/wmmessage.rs`).\n\n## Details\n\n```rust\nfn write(&mut self, data: &[u8]) -> Result<(), Error> {\n    if self.pos + data.len() > self.length {      // :140  BOUND PRESENT\n        return Err(Error::Overflow);\n    }\n    ... copy_nonoverlapping(&data[0], view+pos, data.len()) ...\n}\n\nfn read(&mut self, n: usize) -> Vec<u8> {         // :160  NO BOUND\n    let out = vec![0; n];                         // n up to 0xFFFF_FFFF (CWE-789)\n    unsafe {\n        std::ptr::copy_nonoverlapping(\n            self.view.Value.add(self.pos) as *const u8,   // view is length==8192\n            out.as_ptr() as *mut u8,\n            n,                                    // reads n bytes, may run past the view (CWE-125)\n        );\n    }\n    self.pos += n;\n    out\n}\n```\n\n`query_pageant_direct` creates the mapping at `_AGENT_MAX_MSGLEN = 8192`, writes\nthe request, sends the `WM_COPYDATA`, then reads the response the peer wrote:\n\n```rust\nmap.seek(0);\nlet mut buf = map.read(4);\nlet size = u32::from_be_bytes([buf[0],buf[1],buf[2],buf[3]]) as usize; // :233 peer-controlled\nbuf.extend(map.read(size));                                            // :234 unbounded\n```\n\nNothing checks `4 + size <= 8192`, so `map.read(size)` runs past the 8 KiB view.\n\n## Proof of concept\n\nBecause the bug is Windows-only (`WM_COPYDATA` + `MapViewOfFile`), the PoC is a\nWindows cross-build (`x86_64-pc-windows-gnu`) driven under **Wine**, entirely\ninside a Linux container. It exercises the **real, unmodified** crate: the PoC\ntakes a path dependency on `pageant` and calls\n`pageant::wmmessage::query_pageant_direct`, exactly what\n`AgentClient::connect_pageant` uses. A second thread impersonates Pageant\n(registers the window class + title `\"Pageant\"`) and, on `WM_COPYDATA`, opens\nthe shared mapping russh created and writes an attacker-chosen 4-byte\nbig-endian length.\n\n`poc/run.sh` builds and runs it. Full log in `results/e2e-wine-run.log`:\n\n```\n======== LEG 1 — ATTACK: fake agent reports length 0x00080000 (512 KiB) >> 8192-byte view ========\n[attacker] impersonating Pageant window is up (class+title \"Pageant\")\n[victim ] calling pageant::wmmessage::query_pageant_direct() over the 8192-byte view ...\n[attacker] WM_COPYDATA received; shared mapping = \"PageantRequestpoc\"\n[attacker] wrote hostile response length = 524288 (0x00080000) into the 8192-byte view\nwine: Unhandled page fault on read access to 0000000002092000 at address 00000002282CFDC4 ...\nWINE-EXIT=5\n\n======== LEG 2 — CONTROL: fake agent reports length 0x00000010 (16 B), in-bounds ========\n[victim ] returned 20 bytes — request+response fit inside the 8192-byte view (in-bounds control); no fault\nWINE-EXIT=0\n```\n\n- **LEG 1 (attack)**: an oversized `size` makes `MemoryMap::read` read past the\n  8192-byte view; Wine reports an unhandled page fault at a page-aligned address\n  (`0x…2092000`) — the out-of-bounds read as an access violation (crash). Exit\n  code 5 = `STATUS_ACCESS_VIOLATION`.\n- **LEG 2 (control)**: an in-bounds `size` returns cleanly. Same code path; the\n  only difference is whether the peer's length exceeds the view — isolating the\n  missing bound.\n\n**Fix validation.** With `patch/pageant-read-bound.patch` applied and the PoC\nrebuilt against the patched crate, the identical attack input is rejected\n(`results/patched-wine-run.log`):\n\n```\n[attacker] wrote hostile response length = 524288 (0x00080000) into the 8192-byte view\n[victim ] query_pageant_direct error: Overflow\nWINE-EXIT=0\n```\n\nNo fault; the read is refused at the bound, exactly as `write` already refuses\noversize writes. (A pure-logic Linux model of the same control flow is also\nincluded as `poc/pageant_read_oob_demo.rs` / `results/logic-demo-run.log`.)\n\n## Remediation\n\nMirror `write`'s guard in `read` and validate the response length before\nallocating/copying. `patch/pageant-read-bound.patch`:\n\n- `MemoryMap::read(n)` returns `Result<Vec<u8>, Error>` and returns\n  `Error::Overflow` when `self.pos + n > self.length`;\n- `query_pageant_direct` propagates that `Result` and additionally rejects\n  `size > _AGENT_MAX_MSGLEN - 4` before `map.read(size)`.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "crates.io",
      "name" : "pageant"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.2.3"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 0.2.2"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/Eugeny/russh/security/advisories/GHSA-g4mp-vgx3-xrvm"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-102820"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/Eugeny/russh/commit/5d566989ebabfdebfe6b33243d31765a0812260b"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/Eugeny/russh"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/Eugeny/russh/releases/tag/v0.63.2"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-125", "CWE-789" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-30T23:41:02Z",
    "nvd_published_at" : "2026-09-29T19:17:23Z"
  }
}