{"id":"CVE-2026-111061","kind":"vulnerability","aliases":["CVE-2026-111061","GHSA-4wc5-f2rc-q74m"],"related":[],"title":"russh: Unbounded memory allocation in agent protocol server via crafted constraint records","description":"### 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.","summary":"The russh Rust SSH library's agent server spawns a task for every lifetime constraint record in an add-key request, with no limit and no rollback on failure. A peer that can talk the agent protocol (a local process on the agent socket, or a malicious SSH server reached via agent forwarding) can exhaust the agent process's memory and kill it, with no confidentiality or integrity impact.","severity":"medium","cvss":4.300000190734863,"epss":0.0034600000362843275,"exploited":false,"exploited_at":null,"products":[{"name":"russh","fixed":["0.64.1"],"affected":"<= 0.64.0","ecosystem":"cargo"}],"ecosystems":["cargo"],"tags":["cwe-770","package","russh","rust","self-hosted","ssh-agent"],"not_affected_if":["agent-forwarding-disabled","no-agent-server-used","russh-below-0.64.1-absent"],"how_to_check":"Run cargo tree or inspect Cargo.lock for russh versions 0.64.0 or earlier.\nCheck whether the application runs russh's agent server (russh_keys::agent::server) or relies on russh clients accepting agent-forwarding channels.","fix":"Upgrade russh to 0.64.1 or later.","status":"open","published_at":"2026-10-19T17:06:46+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T20:03:22+00:00","updated_at":"2026-10-23T20:03:22+00:00","category":"package","confidence":0.7,"triage":{"at":"2026-10-23T20:03:22Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade russh to 0.64.1 or later.\nunknown for a workaround if upgrading is not possible.","summary":"The russh Rust SSH library's agent server spawns a task for every lifetime constraint record in an add-key request, with no limit and no rollback on failure. A peer that can talk the agent protocol (a local process on the agent socket, or a malicious SSH server reached via agent forwarding) can exhaust the agent process's memory and kill it, with no confidentiality or integrity impact.","category":"package","who_cares":["rust","russh","ssh-agent","self-hosted"],"confidence":0.7,"analyst_note":"Confirm the affected crate name and version range against the GHSA-4wc5-f2rc-q74m advisory at https://github.com/advisories/GHSA-4wc5-f2rc-q74m and whether the agent server is reachable in default russh client configurations.","how_to_check":"Run cargo tree or inspect Cargo.lock for russh versions 0.64.0 or earlier.\nCheck whether the application runs russh's agent server (russh_keys::agent::server) or relies on russh clients accepting agent-forwarding channels.\nunknown for any other exposure check.","needs_analyst":true,"not_affected_if":["russh-below-0.64.1-absent","no-agent-server-used","agent-forwarding-disabled"],"severity_if_unknown":"medium"},"cost_usd":0.000643,"input_tokens":4014,"output_tokens":483,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'+1':209C '+1.45':515C '+1087580':465C,645C '+1447856':476C,483C,656C,663C '+268':226C,533C,695C '-01':285C '-10':284C '-111061':15A,23A '-2026':14A,22A '-319':115C,290C '-770':26B '/deployment/deploy.sh':446C '/eugeny/russh':578C '/home/ubuntu/hunttargets/russh':445C '/home/ubuntu/russh-poc/poc':450C '/poc/poc.sh':454C '/target/release/poc-agent-addkey':634C '000':623C '096':204C '1':551C '10':518C '10.2':197C '1090272':462C,642C '120':205C '136':148C,495C,675C '141x':192C '1450548':473C,480C,653C,660C '19':502C '2':203C,599C '2026':283C '2096120':490C,670C '2686':260C '271':114C,289C '29':457C,637C '3':681C '39':468C,648C '40':455C,635C '447':210C '4wc5':18A '5':123C,375C '52':622C '6':460C,471C,509C,640C,651C '856':211C 'accept':252C 'ack':505C 'add':55B,111C,133C,175C,222C,291C,365C,385C,531C,614C,691C 'add-key':54B,174C,221C,690C 'agent':6A,35B,42B,72B,79B,88B,93B,109C,126C,180C,232C,247C,264C,305C,361C,428C,435C,568C,606C,628C 'agentc':364C,613C 'aliv':358C 'alloc':4A 'alreadi':346C 'amplif':193C,486C,666C 'amplifi':392C 'archiv':404C,537C,549C 'async':318C 'attach':130C 'attack':229C 'auth-agent@openssh.com':254C 'autom':536C 'await':324C 'back':165C 'bash':439C,573C,633C 'blob':139C,333C,499C,679C 'broker':504C 'budget':158C 'build':559C,585C 'byte':124C,184C,376C,489C,626C,669C 'capac':270C 'cargo':400C,584C 'cd':579C 'channel':255C 'cheap':274C 'checkout':582C 'client':251C 'clone':136C,330C,575C,598C 'code':459C,470C,508C,639C,650C 'concept':395C 'confidenti':102B 'connect':181C,244C,362C 'constrain':127C,173C,306C,367C,616C,629C 'constraint':11A,50B,293C,368C,388C,619C,702C 'constraint-record':701C 'contain':621C 'control':378C,525C,682C,698C 'convert':182C 'cost':225C 'craft':10A 'crate':417C,562C,590C 'cve':13A,21A 'cwe':25B 'data':359C 'declar':144C 'decod':300C,311C,369C 'dedic':118C 'default':257C 'delta':464C,475C,482C,644C,655C,662C 'depend':421C,594C 'deploy':406C 'detail':286C 'directori':591C 'earlier':356C 'efa9a03':281C,449C 'efa9a03e541b69505d5e6ec0d9213d011c9f6310':583C 'everi':48B,122C,506C 'exact':540C 'exhaust':91B,262C 'f2rc':19A 'fail':170C 'failur':65B,350C 'fds':501C 'final':478C,658C 'five':625C 'five-byt':624C 'flow':360C 'forward':89B,248C 'gb':516C 'ghsa':17A 'ghsa-4wc5-f2rc-q74m':16A 'git':402C,574C,581C 'github.com':577C 'github.com/eugeny/russh':576C 'grow':513C 'har':523C 'head':280C 'heap':189C 'hold':497C,677C 'home':441C,452C 'home/russh-poc':442C,453C 'host':32B,398C,564C 'ident':366C,380C,615C 'impact':105B 'in-process':570C 'input':521C 'integr':104B 'isol':386C,699C 'key':56B,108C,112C,134C,138C,176C,223C,292C,316C,326C,332C,692C 'key.clone':317C 'kib':212C,227C,463C,466C,474C,477C,481C,484C,534C,643C,646C,654C,657C,661C,664C,696C 'kill':98B 'later':351C 'leav':355C 'let':296C,308C,315C 'librari':40B 'lifetim':49B,128C,145C,307C,309C,323C,328C,630C 'limit':60B 'linux':397C 'live':206C 'local':75B,235C 'loop':151C,294C,370C 'loopback':431C,608C 'malici':83B 'mani':341C 'manual':543C 'match':302C 'measur':196C,379C,532C,694C 'memori':3A,96B,267C 'messag':177C,617C 'mib':198C,519C 'mode':526C,697C 'move':319C 'new':336C 'ok':297C 'one':172C 'overal':168C 'packag':27B 'path':389C,420C,593C 'peer':67B 'per':155C,374C 'per-request':154C 'pin':412C,556C 'poc':416C,423C,440C,451C,561C,589C,602C 'point':595C 'prerequisit':396C 'process':76B,94B,236C,265C,572C 'produc':202C 'proof':393C 'protocol':7A,73B,200C,436C 'provis':408C,552C 'q74m':20A 'r':301C,312C 'reach':86B,230C 'real':426C,566C 'record':12A,51B,129C,329C,342C,631C,703C 'regardless':338C 'releas':586C 'remot':239C 'remov':325C 'renew':273C 'repeat':171C,611C 'repli':458C,469C,638C,649C 'repo':444C 'reproduc':539C 'reproduct':544C 'request':57B,156C,169C,224C,275C,345C,354C,507C,693C 'requir':550C 'resid':186C 'revis':413C,557C 'rewrit':418C 'roll':164C 'rollback':63B 'rough':191C 'round':456C,467C,636C,647C 'rss':213C,461C,472C,479C,512C,641C,652C,659C 'run':424C,538C,600C 'russh':1A,24A,28B,37B,107C,250C,279C,409C,427C,443C,447C,553C,567C,580C 'russh/src/client/mod.rs':259C 'russh/src/keys/agent/server.rs':113C,288C 'rust':29B,38B,287C 'script':407C 'section':620C 'self':31B 'self-host':30B 'send':610C 'sent':218C,687C 'server':8A,43B,85B,110C,241C,429C,569C 'sinc':249C 'sleep':141C,322C,493C,673C 'socket':80B,233C 'spawn':44B,116C,160C,314C,347C,373C 'speak':433C 'ssh':34B,39B,84B,125C,240C,304C,363C,612C,627C 'ssh-agent':33B 'start':604C 'step':545C,547C 'stream':276C 'success':510C 'summari':106C 'talk':70B 'task':46B,120C,157C,161C,187C,207C,269C,337C,357C,491C,671C,704C 'time':321C 'timer':500C,680C 'tokio':119C,313C,320C,372C 'trace':542C 'traffic':201C,217C 'u32':310C 'u8':299C 'unbound':2A,371C 'unconstrain':220C,384C,530C,689C 'user':243C 'v0.63.3-10':282C 'verifi':277C 'via':9A,87B 'volum':382C,529C,686C 'whose':618C 'wire':183C,381C,488C,528C,668C,685C 'x141':485C,665C 'year':149C,496C,676C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-19T17:06:46+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-4wc5-f2rc-q74m","url":"https://github.com/advisories/GHSA-4wc5-f2rc-q74m","severity":"medium","published_at":"2026-10-19T17:06:46+00:00","modified_at":"2026-10-19T17:06:48+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","fetched_at":"2026-10-23T19:11:32+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-111061","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-111061","severity":null,"published_at":"2026-10-19T17:17:03+00:00","modified_at":"2026-10-19T19:17:22+00:00","first_seen_at":"2026-10-23T21:57:18+00:00","fetched_at":"2026-10-23T22:28:57+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}