CVE-2026-111061: russh agent server converts constraint records into unbounded never-rolled-back tasks

Published Oct 19, 2026
·
Updated

Summary

russhkeys::agent::server::addkey (russh/src/keys/agent/server.rs:271-319) spawns a dedicated tokio task for every 5-byte SSHAGENTCONSTRAINLIFETIME 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

rust // russh/src/keys/agent/server.rs:271-319 (addkey, constraint loop) while let Ok(t) = u8::decode(r) { match t { SSHAGENTCONSTRAINLIFETIME => { 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 -> SSHAGENTCADDIDENTITYCONSTRAINED -> 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.

bash $ POCHOME=$HOME/russh-poc RUSSHREPO=/home/ubuntu/HuntTargets/russh ./deployment/deploy.sh russh at efa9a03 -> /home/ubuntu/russh-poc/poc

$ POCHOME=$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)

1. Provision russh at the pinned revision and build the PoC crate (it hosts a real russh agent server in-process):

bash 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

2. Run the PoC: it starts the agent on loopback, then sends repeated SSHAGENTCADDIDENTITYCONSTRAINED messages whose constraint sections contain ~52,000 five-byte SSHAGENTCONSTRAINLIFETIME records each:

bash ./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.

3. 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.

4. 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.

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.

Other sources

russh is a Rust SSH client and server library that includes an SSH agent protocol implementation. Prior to 0.64.1, the agent server spawns a separate long-lived task for every SSHAGENTCONSTRAINLIFETIME record in a constrained add-key request. The loop accepts repeated lifetime records, clones the key blob into each task, and does not roll back tasks when later constraint parsing fails. An attacker that can speak the agent protocol locally or through agent forwarding can exhaust memory and tasks until the agent process is starved or terminated, denying SSH agent service. This issue is fixed in version 0.64.1.

— MITRE

Affected Software

2 affected componentsFixes available
cargo/russh<0.64.1
rust/russh<=0.64.0
0.64.1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade rust/russh to a version that resolves this vulnerability.

    Fixed in 0.64.1
  2. Upgrade

    Upgrade russh to a version that resolves this vulnerability.

    Fixed in 0.64.1

Event History

Oct 19, 2026
CVE Published
via MITRE·04:30 PM
Data Sourced
via MITRE·04:30 PM
DescriptionSeverityWeakness
Advisory Published
via GitHub·05:06 PM
Data Sourced
via GitHub·05:06 PM
DescriptionSeverityWeaknessAffected Software
Data Sourced
via NVD·05:17 PM
DescriptionSeverityWeakness

Frequently Asked Questions

1

Who can exploit this issue?

An attacker must be able to send SSH agent protocol requests, either locally or through SSH agent forwarding. No authentication or user interaction is required once that access exists.

2

Are affected deployments vulnerable by default?

The issue affects russh agent server implementations before 0.64.1 when they process constrained add-key requests containing SSH_AGENT_CONSTRAIN_LIFETIME records. Exposure depends on whether an attacker can reach the agent protocol endpoint locally or through forwarding.

3

What is the impact of successful exploitation?

Repeated lifetime constraints can create unbounded long-lived tasks and duplicate key blobs in memory. This can exhaust process memory and task capacity, starving or terminating the agent and denying SSH agent service.

4

What should be done if upgrading is not immediately possible?

Restrict access to the agent protocol endpoint and avoid forwarding the agent to untrusted systems, since both local protocol access and agent forwarding can provide an attack path. Upgrade to russh 0.64.1 when possible.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203