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).
// 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.
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.
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
SSH_AGENTC_ADD_IDENTITY_CONSTRAINED messages whose constraint sections contain ~52,000 five-byte SSH_AGENT_CONSTRAIN_LIFETIME records 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.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.
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.
{
"cwe_ids": [
"CWE-770"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-19T17:06:46Z",
"nvd_published_at": null,
"severity": "MODERATE"
}