{"id":"CVE-2026-102821","aliases":["DEBIAN-CVE-2026-102821","EUVD-2026-89204","GHSA-35g8-35p8-c8fw","RHSA-2026:74586","UBUNTU-CVE-2026-102821"],"status":"published","title":"Russh: Unbounded memory exhaustion via CHANNEL_OPEN flood during a client-stalled rekey","description":"Russh is a Rust SSH client and server library. Prior to 0.63.2, an authenticated remote peer can send SSH_MSG_KEXINIT without the required SSH_MSG_KEX_ECDH_INIT and then flood SSH_MSG_CHANNEL_OPEN messages while SessionKexState::InProgress prevents priority_receiver in russh/src/server/session.rs from being drained. The server continues processing network input and enqueues a ChannelOpenReply for each request on an unbounded channel, allowing one connection to grow memory until the process is terminated. This issue is fixed in version 0.63.2.","severity":{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.00358,"percentile":0.29996,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-400","CWE-770"],"published":"2026-09-29T18:21:21Z","modified":"2026-10-28T02:30:48Z","fixes":[{"ecosystem":"crates.io","package":"russh","fixed_in":["0.63.2"],"sources":["ghsa","osv"]},{"ecosystem":"Debian:14","package":"rust-russh","fixed_in":["0.64.1-1"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"crates.io","package":"russh","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"0.63.1"},{"fixed":"0.63.2"}]}]},{"source":"osv","ecosystem":"Debian:14","package":"rust-russh","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.64.1-1"}]}],"versions":["0.57.1-1","0.57.1-2"]},{"source":"osv","ecosystem":"crates.io","package":"russh","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.63.2"}]}]},{"source":"osv","ecosystem":"Red Hat:hummingbird:1","package":"openshell","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0:0.1.2-0.2.hum1"}]}],"bundled":4},{"source":"osv","ecosystem":"Red Hat:hummingbird:1","package":"openshell-gateway","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0:0.1.2-0.2.hum1"}]}],"bundled":4},{"source":"osv","ecosystem":"Red Hat:hummingbird:1","package":"python3-openshell","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0:0.1.2-0.2.hum1"}]}],"bundled":4},{"source":"osv","ecosystem":"Ubuntu:26.10","package":"rust-russh","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.57.1-2"]}],"products":[{"vendor":"eugeny","product":"russh"}],"references":[{"url":"https://github.com/Eugeny/russh/security/advisories/GHSA-35g8-35p8-c8fw","tags":["x_refsource_CONFIRM","exploit","advisory","web","article","report"]},{"url":"https://github.com/Eugeny/russh/commit/a282af361ac99bc76b80876d1aae128e89dbf66b","tags":["x_refsource_MISC","fix","web","article","report"]},{"url":"https://github.com/Eugeny/russh/releases/tag/v0.63.2","tags":["x_refsource_MISC","web","article","report"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-102821","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-35g8-35p8-c8fw"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/102xxx/CVE-2026-102821.json","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-102821","tags":["advisory"]},{"url":"https://github.com/Eugeny/russh","tags":["package"]},{"url":"https://access.redhat.com/errata/RHSA-2026:74586","tags":["advisory"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-102821","tags":["report"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-102823","tags":["report"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-102824","tags":["report"]},{"url":"https://access.redhat.com/security/updates/classification/","tags":["article"]},{"url":"https://images.redhat.com/","tags":["article"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-102825","tags":["report"]},{"url":"https://security.access.redhat.com/data/csaf/v2/advisories/2026/rhsa-2026_74586.json","tags":["advisory"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2543501","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-102821","tags":["advisory","report"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2543503","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-102823","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-102823","tags":["advisory"]},{"url":"https://github.com/Eugeny/russh/commit/3430fd26ecafc0dc3705210f5f39a9119fa22774","tags":["article"]},{"url":"https://github.com/Eugeny/russh/releases/tag/v0.63.1","tags":["article"]},{"url":"https://github.com/Eugeny/russh/security/advisories/GHSA-47hw-gvq5-r2gm","tags":["article"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2543498","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-102824","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-102824","tags":["advisory"]},{"url":"https://github.com/Eugeny/russh/commit/8da8967f196472576b1565d518a0ed60fce60f0c","tags":["article"]},{"url":"https://github.com/Eugeny/russh/releases/tag/v0.63.0","tags":["article"]},{"url":"https://github.com/Eugeny/russh/security/advisories/GHSA-w3jg-pjxf-73p4","tags":["article"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2543507","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-102825","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-102825","tags":["advisory"]},{"url":"https://github.com/Eugeny/russh/commit/f8fd0b11a393364dc7f01a182482d86adafdd653","tags":["article"]},{"url":"https://github.com/Eugeny/russh/releases/tag/v0.62.6","tags":["article"]},{"url":"https://github.com/Eugeny/russh/security/advisories/GHSA-g6xm-f9xp-qq35","tags":["article"]},{"url":"https://ubuntu.com/security/CVE-2026-102821","tags":["report"]}],"sources":{"cve":[{"id":"CVE-2026-102821","title":"Russh: Unbounded memory exhaustion via CHANNEL_OPEN flood during a client-stalled rekey","description":"Russh is a Rust SSH client and server library. Prior to 0.63.2, an authenticated remote peer can send SSH_MSG_KEXINIT without the required SSH_MSG_KEX_ECDH_INIT and then flood SSH_MSG_CHANNEL_OPEN messages while SessionKexState::InProgress prevents priority_receiver in russh/src/server/session.rs from being drained. The server continues processing network input and enqueues a ChannelOpenReply for each request on an unbounded channel, allowing one connection to grow memory until the process is terminated. This issue is fixed in version 0.63.2.","status":"published","published":"2026-09-29T18:21:21.858Z","modified":"2026-09-30T19:35:02.962Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium","assessor":"cna"}],"cwes":["CWE-400","CWE-770"],"updated_at":"2026-11-02T13:38:54.394896Z"}],"euvd":[{"id":"EUVD-2026-89204","description":"Russh is a Rust SSH client and server library. Prior to 0.63.2, an authenticated remote peer can send SSH_MSG_KEXINIT without the required SSH_MSG_KEX_ECDH_INIT and then flood SSH_MSG_CHANNEL_OPEN messages while SessionKexState::InProgress prevents priority_receiver in russh/src/server/session.rs from being drained. The server continues processing network input and enqueues a ChannelOpenReply for each request on an unbounded channel, allowing one connection to grow memory until the process is terminated. This issue is fixed in version 0.63.2.","status":"published","published":"2026-09-29T18:21:21Z","modified":"2026-09-30T19:35:02Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"updated_at":"2026-11-02T17:27:47.733217Z"}],"ghsa":[{"id":"GHSA-35g8-35p8-c8fw","title":"Russh: Unbounded memory exhaustion via CHANNEL_OPEN flood during a client-stalled rekey","description":"## Summary\n\nA russh **server** can be driven to unbounded heap growth (process OOM / kill) by\na peer that speaks only standard SSH messages, in the **default configuration**.\n\nThe peer starts a key re-exchange (sends `SSH_MSG_KEXINIT`) but never sends the\nfollow-up `SSH_MSG_KEX_ECDH_INIT`, leaving the server's kex state machine in\n`SessionKexState::InProgress` **indefinitely**. While a rekey is in progress the\nserver's three message-drain paths are all gated off (`if !self.kex.active()`),\nbut the network-read path stays active, so every `SSH_MSG_CHANNEL_OPEN` the peer\nsends is processed inline and appends one reply to an **unbounded** internal\nqueue (`priority_receiver`, an `UnboundedReceiver`) that is not dequeued until\nthe rekey completes. Because the peer decides whether the rekey ever completes,\nthe queue — and the server's memory — grows without bound.\n\nThis is reproducible end-to-end against a real russh server over a real\nencrypted transport; see \"Proof of concept\". A one-line negative control (same\nflood, no rekey) keeps memory flat, isolating the rekey window as the sole\ntrigger.\n\n## Impact\n\n- **Availability / DoS.** Server RSS climbs at roughly 3.3 KB per\n  `CHANNEL_OPEN`, driven entirely by the peer, until the process is OOM-killed.\n  In the reproduction one connection pushed the server from ~4 MB to **2.57 GB**\n  (and past **4.8 GB** against the stock `echoserver` example) and it was still\n  climbing when the flood was stopped.\n- **Reached with standard messages, no special configuration.** Any handler is\n  affected, including one that rejects every channel (the reply enqueued on\n  rejection is exactly what accumulates). There is no per-connection cap on\n  in-flight channel opens or on the queue, and the queue's sender has no\n  backpressure.\n- The peer keeps the connection alive simply by continuing to send, so the\n  inactivity timer never fires.\n\n## Affected component\n\n- `russh/src/server/session.rs` — `Session::run` `tokio::select!` loop. The\n  network-read arm is ungated; the three drain paths are gated on\n  `!self.kex.active()`.\n- `russh/src/server/mod.rs` — `reply()` routes a non-kex message received during\n  a rekey straight to `server_read_encrypted` (inline processing).\n- `russh/src/lib_inner.rs` — `ChannelOpenHandleInner`'s `accept`/`reject`/`Drop`\n  all `send` a reply on an `UnboundedSender`.\n\nVerified on **v0.63.1** (commit `d3ae702`), which is the latest release. The\ngating logic predates it.\n\n## Details\n\n`Session::run` (`russh/src/server/session.rs:631`) drives a `tokio::select!`\n(`:713`). Three of its message-drain sites are gated on `!self.kex.active()`:\n\n- the pre-`select!` batch drain of `priority_receiver`/`receiver`\n  (`session.rs:680`),\n- the `priority_receiver.recv()` arm (`session.rs:762`),\n- the `receiver.recv()` arm (`session.rs:770`, which also holds the only other\n  `priority_receiver` drain at `:777`).\n\nThe fourth arm, `r = \u0026mut reading` (`session.rs:714`), is **ungated**: it reads\nand processes one incoming packet every loop iteration regardless of rekey\nstate, calling `reply()` (`server/mod.rs:1128`).\n\nDuring a **rekey**, `session.common.encrypted.is_some()`, so the strict-kex\nmessage-ordering guard (`server/mod.rs:1143`, which is additionally gated on\n`encrypted.is_none()`) does **not** apply. A non-kex message therefore falls\nthrough `reply()` to `session.server_read_encrypted(handler, pkt)`\n(`server/mod.rs:1232`) and is handled inline. For `SSH_MSG_CHANNEL_OPEN` this\nreaches the channel-open handling, which hands the application a\n`ChannelOpenHandle`.\n\nWhether the handler accepts or (the trait default) rejects, the handle's\n`accept` / `reject` / `Drop` all `send` a `Msg::ChannelOpenReply` on an\n`UnboundedSender` (`russh/src/lib_inner.rs:560-603`; `Drop` sends\n`AdministrativelyProhibited` at `:594-603`). That sender feeds\n`priority_receiver`, declared `UnboundedReceiver\u003cMsg\u003e` (`session.rs:23`) and\ncreated with `tokio::sync::mpsc::unbounded_channel()` (`session.rs:1522`). Its\nonly drain sites are the three arms gated off during the rekey. So each\n`CHANNEL_OPEN` processed during the rekey window appends one reply (carrying a\n`PendingChannelOpen` = channel params + mpsc `ChannelRef` + ids, a few KB\nretained in practice) to a queue that is never dequeued.\n\nTwo facts make this unbounded and remote:\n\n1. The server enters `InProgress` the moment it receives the peer's `KEXINIT`\n   (`server/mod.rs:1153-1158`, `begin_rekey`) and only leaves it upon receiving\n   the peer's `KEX_ECDH_INIT`. **The peer decides** whether to ever send that,\n   so the window is attacker-held.\n2. There is no per-connection cap on channels or on the priority queue, and the\n   sender is unbounded (no backpressure).\n\n### Root cause\n\nThe intended design was to *buffer* packets received during a rekey and replay\nthem afterwards: the fields `pending_reads: Vec\u003cVec\u003cu8\u003e\u003e` and `pending_len: u32`\n(`session.rs:26-27`) exist and are **drained** at kex completion\n(`server/mod.rs:1194-1198`). But nothing ever **pushes** to `pending_reads` or\nincrements `pending_len` (they are dead — confirmed by grep across\n`russh/src/`). Instead of being buffered, channel messages received during a\nrekey are processed inline, and their replies pile up in the unbounded\n`priority_receiver`. The missing piece is a bound on — or bounded deferral of —\nchannel processing while `kex.active()`.\n\n## Proof of concept\n\nEverything runs inside a container; nothing touches the host.\n\n**Lab.** `poc/Dockerfile` builds `russh-lab:head` from Eugeny/russh @ `d3ae702`\n(v0.63.1), default features (rust 1.91). A raw-SSH-client PoC\n(`poc/poc_rekey_dos.rs`) implements curve25519-sha256 / ssh-ed25519 /\naes256-ctr / hmac-sha2-256 by hand, completes the handshake and a `publickey`\nauth, then:\n\n1. sends `SSH_MSG_KEXINIT` (server enters `kex.active()`),\n2. **never** sends `KEX_ECDH_INIT` (rekey stalls, attacker-held),\n3. floods `SSH_MSG_CHANNEL_OPEN`.\n\nA minimal server (`poc/poc_server.rs`) uses the **trait-default**\n`channel_open_session` (which rejects by dropping the handle), so the measured\ngrowth is purely the undrained priority queue, not accepted-channel state.\n\n`poc/run.sh` runs the attack leg and an identical **negative control** with no\nrekey. Fresh run inside the lab, `N = 800,000` opens\n(`results/rerun-2026-08-29.log`):\n\n```\n=== ATTACK (rekey stall) === (poc_server baseline_RSS=4208KB, N=800000)\n  t=2s  server_RSS=667760KB\n  t=4s  server_RSS=1599600KB\n  t=6s  server_RSS=2556016KB\n  t=8s  server_RSS=2566256KB   (flood done; memory retained)\n=== CONTROL (no rekey) === (poc_server baseline_RSS=4208KB, N=800000)\n  t=2s..t=12s  server_RSS=4208KB   (flat throughout)\n```\n\n- **ATTACK**: RSS `4,208 KB → 2,566,256 KB` (~2.57 GB) and retained after the\n  flood ends — ~3.3 KB per `CHANNEL_OPEN`, attacker-driven. (An earlier canonical\n  run reached 2.7 GB at 800k opens, and past 4.8 GB against the stock\n  `echoserver` example at 1.5 M opens — see `results/canonical-run.log`.)\n- **CONTROL**: RSS flat at `4,208 KB`. Without the rekey window the replies are\n  drained normally; TCP backpressure (the client never reads the failure\n  replies) even throttles the flood.\n\nThe control isolates the rekey window as the sole trigger. Both legs exercise\nthe real server entry point (`Session::run` → `reply` → `server_read_encrypted`)\nover a real encrypted transport.\n\nReproduce: `C=russh-lab N=800000 ./poc/run.sh` (see `poc/POC-README.md`).\n\n## Remediation\n\nThe priority queue carries locally generated channel-open replies, which are\nnon-kex messages the server **must not send** during a rekey anyway (RFC 4253\n§7.1). So the fix is to bound how much channel work is done during a rekey, not\nto drain the queue mid-rekey. Any of:\n\n- **(recommended, minimal)** cap the number of non-kex messages processed while a\n  rekey is in progress and disconnect a peer that exceeds it. A stalled/abusive\n  rekey is then torn down after a small constant instead of growing memory\n  without bound. See `patch/rekey-message-cap.patch` (a ~10-line change local to\n  `Session::run`, validated in the lab — the attack leg is disconnected after the\n  cap and RSS stays flat; a normal rekey, which completes in one round trip, is\n  unaffected).\n- bound / deferred-buffer channel messages during a rekey using the existing\n  (currently dead) `pending_reads`/`pending_len` machinery, with a hard cap.\n- bound the number of in-flight channel opens per connection and reject beyond it.\n\nOpenSSH does not service new channels mid-kex; matching that intent closes the\nwhole family.","status":"published","published":"2026-09-30T23:40:43Z","modified":"2026-09-30T23:40:43Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"cwes":["CWE-400","CWE-770"],"updated_at":"2026-11-02T14:41:23.783773Z"}],"nvd":[{"id":"CVE-2026-102821","description":"Russh is a Rust SSH client and server library. Prior to 0.63.2, an authenticated remote peer can send SSH_MSG_KEXINIT without the required SSH_MSG_KEX_ECDH_INIT and then flood SSH_MSG_CHANNEL_OPEN messages while SessionKexState::InProgress prevents priority_receiver in russh/src/server/session.rs from being drained. The server continues processing network input and enqueues a ChannelOpenReply for each request on an unbounded channel, allowing one connection to grow memory until the process is terminated. This issue is fixed in version 0.63.2.","status":"published","published":"2026-09-29T19:17:23.993Z","modified":"2026-09-30T20:17:23.793Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium","assessor":"secondary"}],"cwes":["CWE-400","CWE-770"],"updated_at":"2026-11-02T14:12:52.759595Z"}],"osv":[{"id":"CVE-2026-102821","title":"Russh: Unbounded memory exhaustion via CHANNEL_OPEN flood during a client-stalled rekey","description":"Russh is a Rust SSH client and server library. Prior to 0.63.2, an authenticated remote peer can send SSH_MSG_KEXINIT without the required SSH_MSG_KEX_ECDH_INIT and then flood SSH_MSG_CHANNEL_OPEN messages while SessionKexState::InProgress prevents priority_receiver in russh/src/server/session.rs from being drained. The server continues processing network input and enqueues a ChannelOpenReply for each request on an unbounded channel, allowing one connection to grow memory until the process is terminated. This issue is fixed in version 0.63.2.","status":"published","published":"2026-09-29T18:21:21.858Z","modified":"2026-10-28T02:30:48.009871949Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"cwes":["CWE-400","CWE-770"],"updated_at":"2026-11-02T15:44:53.83521Z"},{"id":"DEBIAN-CVE-2026-102821","description":"Russh is a Rust SSH client and server library. Prior to 0.63.2, an authenticated remote peer can send SSH_MSG_KEXINIT without the required SSH_MSG_KEX_ECDH_INIT and then flood SSH_MSG_CHANNEL_OPEN messages while SessionKexState::InProgress prevents priority_receiver in russh/src/server/session.rs from being drained. The server continues processing network input and enqueues a ChannelOpenReply for each request on an unbounded channel, allowing one connection to grow memory until the process is terminated. This issue is fixed in version 0.63.2.","status":"published","published":"2026-09-29T19:17:23.993Z","modified":"2026-10-14T16:00:05.971919474Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"updated_at":"2026-11-02T15:45:48.903138Z"},{"id":"GHSA-35g8-35p8-c8fw","title":"Russh: Unbounded memory exhaustion via CHANNEL_OPEN flood during a client-stalled rekey","description":"## Summary\n\nA russh **server** can be driven to unbounded heap growth (process OOM / kill) by\na peer that speaks only standard SSH messages, in the **default configuration**.\n\nThe peer starts a key re-exchange (sends `SSH_MSG_KEXINIT`) but never sends the\nfollow-up `SSH_MSG_KEX_ECDH_INIT`, leaving the server's kex state machine in\n`SessionKexState::InProgress` **indefinitely**. While a rekey is in progress the\nserver's three message-drain paths are all gated off (`if !self.kex.active()`),\nbut the network-read path stays active, so every `SSH_MSG_CHANNEL_OPEN` the peer\nsends is processed inline and appends one reply to an **unbounded** internal\nqueue (`priority_receiver`, an `UnboundedReceiver`) that is not dequeued until\nthe rekey completes. Because the peer decides whether the rekey ever completes,\nthe queue — and the server's memory — grows without bound.\n\nThis is reproducible end-to-end against a real russh server over a real\nencrypted transport; see \"Proof of concept\". A one-line negative control (same\nflood, no rekey) keeps memory flat, isolating the rekey window as the sole\ntrigger.\n\n## Impact\n\n- **Availability / DoS.** Server RSS climbs at roughly 3.3 KB per\n  `CHANNEL_OPEN`, driven entirely by the peer, until the process is OOM-killed.\n  In the reproduction one connection pushed the server from ~4 MB to **2.57 GB**\n  (and past **4.8 GB** against the stock `echoserver` example) and it was still\n  climbing when the flood was stopped.\n- **Reached with standard messages, no special configuration.** Any handler is\n  affected, including one that rejects every channel (the reply enqueued on\n  rejection is exactly what accumulates). There is no per-connection cap on\n  in-flight channel opens or on the queue, and the queue's sender has no\n  backpressure.\n- The peer keeps the connection alive simply by continuing to send, so the\n  inactivity timer never fires.\n\n## Affected component\n\n- `russh/src/server/session.rs` — `Session::run` `tokio::select!` loop. The\n  network-read arm is ungated; the three drain paths are gated on\n  `!self.kex.active()`.\n- `russh/src/server/mod.rs` — `reply()` routes a non-kex message received during\n  a rekey straight to `server_read_encrypted` (inline processing).\n- `russh/src/lib_inner.rs` — `ChannelOpenHandleInner`'s `accept`/`reject`/`Drop`\n  all `send` a reply on an `UnboundedSender`.\n\nVerified on **v0.63.1** (commit `d3ae702`), which is the latest release. The\ngating logic predates it.\n\n## Details\n\n`Session::run` (`russh/src/server/session.rs:631`) drives a `tokio::select!`\n(`:713`). Three of its message-drain sites are gated on `!self.kex.active()`:\n\n- the pre-`select!` batch drain of `priority_receiver`/`receiver`\n  (`session.rs:680`),\n- the `priority_receiver.recv()` arm (`session.rs:762`),\n- the `receiver.recv()` arm (`session.rs:770`, which also holds the only other\n  `priority_receiver` drain at `:777`).\n\nThe fourth arm, `r = \u0026mut reading` (`session.rs:714`), is **ungated**: it reads\nand processes one incoming packet every loop iteration regardless of rekey\nstate, calling `reply()` (`server/mod.rs:1128`).\n\nDuring a **rekey**, `session.common.encrypted.is_some()`, so the strict-kex\nmessage-ordering guard (`server/mod.rs:1143`, which is additionally gated on\n`encrypted.is_none()`) does **not** apply. A non-kex message therefore falls\nthrough `reply()` to `session.server_read_encrypted(handler, pkt)`\n(`server/mod.rs:1232`) and is handled inline. For `SSH_MSG_CHANNEL_OPEN` this\nreaches the channel-open handling, which hands the application a\n`ChannelOpenHandle`.\n\nWhether the handler accepts or (the trait default) rejects, the handle's\n`accept` / `reject` / `Drop` all `send` a `Msg::ChannelOpenReply` on an\n`UnboundedSender` (`russh/src/lib_inner.rs:560-603`; `Drop` sends\n`AdministrativelyProhibited` at `:594-603`). That sender feeds\n`priority_receiver`, declared `UnboundedReceiver\u003cMsg\u003e` (`session.rs:23`) and\ncreated with `tokio::sync::mpsc::unbounded_channel()` (`session.rs:1522`). Its\nonly drain sites are the three arms gated off during the rekey. So each\n`CHANNEL_OPEN` processed during the rekey window appends one reply (carrying a\n`PendingChannelOpen` = channel params + mpsc `ChannelRef` + ids, a few KB\nretained in practice) to a queue that is never dequeued.\n\nTwo facts make this unbounded and remote:\n\n1. The server enters `InProgress` the moment it receives the peer's `KEXINIT`\n   (`server/mod.rs:1153-1158`, `begin_rekey`) and only leaves it upon receiving\n   the peer's `KEX_ECDH_INIT`. **The peer decides** whether to ever send that,\n   so the window is attacker-held.\n2. There is no per-connection cap on channels or on the priority queue, and the\n   sender is unbounded (no backpressure).\n\n### Root cause\n\nThe intended design was to *buffer* packets received during a rekey and replay\nthem afterwards: the fields `pending_reads: Vec\u003cVec\u003cu8\u003e\u003e` and `pending_len: u32`\n(`session.rs:26-27`) exist and are **drained** at kex completion\n(`server/mod.rs:1194-1198`). But nothing ever **pushes** to `pending_reads` or\nincrements `pending_len` (they are dead — confirmed by grep across\n`russh/src/`). Instead of being buffered, channel messages received during a\nrekey are processed inline, and their replies pile up in the unbounded\n`priority_receiver`. The missing piece is a bound on — or bounded deferral of —\nchannel processing while `kex.active()`.\n\n## Proof of concept\n\nEverything runs inside a container; nothing touches the host.\n\n**Lab.** `poc/Dockerfile` builds `russh-lab:head` from Eugeny/russh @ `d3ae702`\n(v0.63.1), default features (rust 1.91). A raw-SSH-client PoC\n(`poc/poc_rekey_dos.rs`) implements curve25519-sha256 / ssh-ed25519 /\naes256-ctr / hmac-sha2-256 by hand, completes the handshake and a `publickey`\nauth, then:\n\n1. sends `SSH_MSG_KEXINIT` (server enters `kex.active()`),\n2. **never** sends `KEX_ECDH_INIT` (rekey stalls, attacker-held),\n3. floods `SSH_MSG_CHANNEL_OPEN`.\n\nA minimal server (`poc/poc_server.rs`) uses the **trait-default**\n`channel_open_session` (which rejects by dropping the handle), so the measured\ngrowth is purely the undrained priority queue, not accepted-channel state.\n\n`poc/run.sh` runs the attack leg and an identical **negative control** with no\nrekey. Fresh run inside the lab, `N = 800,000` opens\n(`results/rerun-2026-08-29.log`):\n\n```\n=== ATTACK (rekey stall) === (poc_server baseline_RSS=4208KB, N=800000)\n  t=2s  server_RSS=667760KB\n  t=4s  server_RSS=1599600KB\n  t=6s  server_RSS=2556016KB\n  t=8s  server_RSS=2566256KB   (flood done; memory retained)\n=== CONTROL (no rekey) === (poc_server baseline_RSS=4208KB, N=800000)\n  t=2s..t=12s  server_RSS=4208KB   (flat throughout)\n```\n\n- **ATTACK**: RSS `4,208 KB → 2,566,256 KB` (~2.57 GB) and retained after the\n  flood ends — ~3.3 KB per `CHANNEL_OPEN`, attacker-driven. (An earlier canonical\n  run reached 2.7 GB at 800k opens, and past 4.8 GB against the stock\n  `echoserver` example at 1.5 M opens — see `results/canonical-run.log`.)\n- **CONTROL**: RSS flat at `4,208 KB`. Without the rekey window the replies are\n  drained normally; TCP backpressure (the client never reads the failure\n  replies) even throttles the flood.\n\nThe control isolates the rekey window as the sole trigger. Both legs exercise\nthe real server entry point (`Session::run` → `reply` → `server_read_encrypted`)\nover a real encrypted transport.\n\nReproduce: `C=russh-lab N=800000 ./poc/run.sh` (see `poc/POC-README.md`).\n\n## Remediation\n\nThe priority queue carries locally generated channel-open replies, which are\nnon-kex messages the server **must not send** during a rekey anyway (RFC 4253\n§7.1). So the fix is to bound how much channel work is done during a rekey, not\nto drain the queue mid-rekey. Any of:\n\n- **(recommended, minimal)** cap the number of non-kex messages processed while a\n  rekey is in progress and disconnect a peer that exceeds it. A stalled/abusive\n  rekey is then torn down after a small constant instead of growing memory\n  without bound. See `patch/rekey-message-cap.patch` (a ~10-line change local to\n  `Session::run`, validated in the lab — the attack leg is disconnected after the\n  cap and RSS stays flat; a normal rekey, which completes in one round trip, is\n  unaffected).\n- bound / deferred-buffer channel messages during a rekey using the existing\n  (currently dead) `pending_reads`/`pending_len` machinery, with a hard cap.\n- bound the number of in-flight channel opens per connection and reject beyond it.\n\nOpenSSH does not service new channels mid-kex; matching that intent closes the\nwhole family.","status":"published","published":"2026-09-30T23:40:43Z","modified":"2026-10-01T00:00:04.683452538Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"cwes":["CWE-400","CWE-770"],"updated_at":"2026-11-02T15:46:35.050232Z"},{"id":"RHSA-2026:74586","title":"Red Hat Security Advisory: Red Hat Hardened Images RPMs Security Update","status":"published","published":"2026-10-02T10:26:00Z","modified":"2026-10-03T10:16:58Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N","level":"high"}],"updated_at":"2026-11-02T15:53:22.928316Z"},{"id":"UBUNTU-CVE-2026-102821","description":"Russh is a Rust SSH client and server library. Prior to 0.63.2, an authenticated remote peer can send SSH_MSG_KEXINIT without the required SSH_MSG_KEX_ECDH_INIT and then flood SSH_MSG_CHANNEL_OPEN messages while SessionKexState::InProgress prevents priority_receiver in russh/src/server/session.rs from being drained. The server continues processing network input and enqueues a ChannelOpenReply for each request on an unbounded channel, allowing one connection to grow memory until the process is terminated. This issue is fixed in version 0.63.2.","status":"published","published":"2026-09-29T19:17:00Z","modified":"2026-10-17T12:57:56.519278416Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"updated_at":"2026-11-02T16:55:58.519486Z"}]}}
