Public mode

No access to your tenant or devices. Every item links to its source. What this means

Some sources, documents or advisories need attention: 274/275 feeds/APIs · 469/469 docs · 8/9 advisories · checked 02:22 UTC Customize Public modeDiscuss ChangeIntel on Discord

Russh: Unbounded memory exhaustion via CHANNEL_OPEN flood during a client-stalled rekey

Collected exploitation and prediction evidence

Risk source coverage

CISA KEV is observed exploitation evidence. FIRST EPSS is a dated prediction of exploitation in the next 30 days. Missing scores are unknown. This collector requests predictions only for explicit CVE aliases in the public NuGet archive; Microsoft release views retain their existing prediction scope.

No collected risk observation for these identifiers. This does not establish absence of exploitation or a zero probability.

cve-program · CVE-2026-102821

Published

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.

Source published
29 Sep 2026 18:21:21 UTC
Source modified
30 Sep 2026 19:35:02 UTC
Observed here
23 Oct 2026 15:48:27 UTC
Source terms
CVE Program Terms of Use

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: cve-program · CVE Program Terms of Use. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS 3.1 · 6.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

euvd · EUVD-2026-89204

Published

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.

EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON.

Original publisher date fields

datePublished: Sep 29, 2026, 6:21:21 PM

dateUpdated: Sep 30, 2026, 7:35:02 PM

1 related source bulletins · separate from aliases

RHSA-2026:74586: Red Hat Security Advisory: Red Hat Hardened Images RPMs Security Update

CVE-2026-102821 · CVE-2026-102823 · CVE-2026-102824 · CVE-2026-102825

Source published
Sep 29, 2026, 6:21:21 PM (timezone not stated)
Source modified
Sep 30, 2026, 7:35:02 PM (timezone not stated)
Observed here
11 Nov 2026 05:29:26 UTC
Source terms
ENISA EUVD · source terms and attribution apply

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: euvd · ENISA EUVD · source terms and attribution apply. Source text is retained; field grouping and change labels are provided by ChangeIntel.

EUVD CVSS 3.1 · 6.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

nvd · CVE-2026-102821

Published

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.

NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON.

Source analysis status: Deferred

Weakness classifications: CWE-400 · CWE-770

Original publisher date fields

published: 2026-09-29T19:17:23.993

lastModified: 2026-09-30T20:17:23.793

Source published
29 Sep 2026 19:17:23 UTC
Source modified
30 Sep 2026 20:17:23 UTC
Observed here
25 Oct 2026 04:17:26 UTC
Source terms
NIST NVD usage terms; underlying source attribution retained

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: nvd · NIST NVD usage terms; underlying source attribution retained. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS 3.1 · [email protected] · Secondary · 6.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

osv · GHSA-35g8-35p8-c8fw

Published

## Summary A russh **server** can be driven to unbounded heap growth (process OOM / kill) by a peer that speaks only standard SSH messages, in the **default configuration**. The peer starts a key re-exchange (sends `SSH_MSG_KEXINIT`) but never sends the follow-up `SSH_MSG_KEX_ECDH_INIT`, leaving the server's kex state machine in `SessionKexState::InProgress` **indefinitely**. While a rekey is in progress the server's three message-drain paths are all gated off (`if !self.kex.active()`), but the network-read path stays active, so every `SSH_MSG_CHANNEL_OPEN` the peer sends is processed inline and appends one reply to an **unbounded** internal queue (`priority_receiver`, an `UnboundedReceiver`) that is not dequeued until the rekey completes. Because the peer decides whether the rekey ever completes, the queue — and the server's memory — grows without bound. This is reproducible end-to-end against a real russh server over a real encrypted transport; see "Proof of concept". A one-line negative control (same flood, no rekey) keeps memory flat, isolating the rekey window as the sole trigger. ## Impact - **Availability / DoS.** Server RSS climbs at roughly 3.3 KB per `CHANNEL_OPEN`, driven entirely by the peer, until the process is OOM-killed. In the reproduction one connection pushed the server from ~4 MB to **2.57 GB** (and past **4.8 GB** against the stock `echoserver` example) and it was still climbing when the flood was stopped. - **Reached with standard messages, no special configuration.** Any handler is affected, including one that rejects every channel (the reply enqueued on rejection is exactly what accumulates). There is no per-connection cap on in-flight channel opens or on the queue, and the queue's sender has no backpressure. - The peer keeps the connection alive simply by continuing to send, so the inactivity timer never fires. ## Affected component - `russh/src/server/session.rs` — `Session::run` `tokio::select!` loop. The network-read arm is ungated; the three drain paths are gated on `!self.kex.active()`. - `russh/src/server/mod.rs` — `reply()` routes a non-kex message received during a rekey straight to `server_read_encrypted` (inline processing). - `russh/src/lib_inner.rs` — `ChannelOpenHandleInner`'s `accept`/`reject`/`Drop` all `send` a reply on an `UnboundedSender`. Verified on **v0.63.1** (commit `d3ae702`), which is the latest release. The gating logic predates it. ## Details `Session::run` (`russh/src/server/session.rs:631`) drives a `tokio::select!` (`:713`). Three of its message-drain sites are gated on `!self.kex.active()`: - the pre-`select!` batch drain of `priority_receiver`/`receiver` (`session.rs:680`), - the `priority_receiver.recv()` arm (`session.rs:762`), - the `receiver.recv()` arm (`session.rs:770`, which also holds the only other `priority_receiver` drain at `:777`). The fourth arm, `r = &mut reading` (`session.rs:714`), is **ungated**: it reads and processes one incoming packet every loop iteration regardless of rekey state, calling `reply()` (`server/mod.rs:1128`). During a **rekey**, `session.common.encrypted.is_some()`, so the strict-kex message-ordering guard (`server/mod.rs:1143`, which is additionally gated on `encrypted.is_none()`) does **not** apply. A non-kex message therefore falls through `reply()` to `session.server_read_encrypted(handler, pkt)` (`server/mod.rs:1232`) and is handled inline. For `SSH_MSG_CHANNEL_OPEN` this reaches the channel-open handling, which hands the application a `ChannelOpenHandle`. Whether the handler accepts or (the trait default) rejects, the handle's `accept` / `reject` / `Drop` all `send` a `Msg::ChannelOpenReply` on an `UnboundedSender` (`russh/src/lib_inner.rs:560-603`; `Drop` sends `AdministrativelyProhibited` at `:594-603`). That sender feeds `priority_receiver`, declared `UnboundedReceiver<Msg>` (`session.rs:23`) and created with `tokio::sync::mpsc::unbounded_channel()` (`session.rs:1522`). Its only drain sites are the three arms gated off during the rekey. So each `CHANNEL_OPEN` processed during the rekey window appends one reply (carrying a `PendingChannelOpen` = channel params + mpsc `ChannelRef` + ids, a few KB retained in practice) to a queue that is never dequeued. Two facts make this unbounded and remote: 1. The server enters `InProgress` the moment it receives the peer's `KEXINIT` (`server/mod.rs:1153-1158`, `begin_rekey`) and only leaves it upon receiving the peer's `KEX_ECDH_INIT`. **The peer decides** whether to ever send that, so the window is attacker-held. 2. There is no per-connection cap on channels or on the priority queue, and the sender is unbounded (no backpressure). ### Root cause The intended design was to *buffer* packets received during a rekey and replay them afterwards: the fields `pending_reads: Vec<Vec<u8>>` and `pending_len: u32` (`session.rs:26-27`) exist and are **drained** at kex completion (`server/mod.rs:1194-1198`). But nothing ever **pushes** to `pending_reads` or increments `pending_len` (they are dead — confirmed by grep across `russh/src/`). Instead of being buffered, channel messages received during a rekey are processed inline, and their replies pile up in the unbounded `priority_receiver`. The missing piece is a bound on — or bounded deferral of — channel processing while `kex.active()`. ## Proof of concept Everything runs inside a container; nothing touches the host. **Lab.** `poc/Dockerfile` builds `russh-lab:head` from Eugeny/russh @ `d3ae702` (v0.63.1), default features (rust 1.91). A raw-SSH-client PoC (`poc/poc_rekey_dos.rs`) implements curve25519-sha256 / ssh-ed25519 / aes256-ctr / hmac-sha2-256 by hand, completes the handshake and a `publickey` auth, then: 1. sends `SSH_MSG_KEXINIT` (server enters `kex.active()`), 2. **never** sends `KEX_ECDH_INIT` (rekey stalls, attacker-held), 3. floods `SSH_MSG_CHANNEL_OPEN`. A minimal server (`poc/poc_server.rs`) uses the **trait-default** `channel_open_session` (which rejects by dropping the handle), so the measured growth is purely the undrained priority queue, not accepted-channel state. `poc/run.sh` runs the attack leg and an identical **negative control** with no rekey. Fresh run inside the lab, `N = 800,000` opens (`results/rerun-2026-08-29.log`): ``` === ATTACK (rekey stall) === (poc_server baseline_RSS=4208KB, N=800000) t=2s server_RSS=667760KB t=4s server_RSS=1599600KB t=6s server_RSS=2556016KB t=8s server_RSS=2566256KB (flood done; memory retained) === CONTROL (no rekey) === (poc_server baseline_RSS=4208KB, N=800000) t=2s..t=12s server_RSS=4208KB (flat throughout) ``` - **ATTACK**: RSS `4,208 KB → 2,566,256 KB` (~2.57 GB) and retained after the flood ends — ~3.3 KB per `CHANNEL_OPEN`, attacker-driven. (An earlier canonical run reached 2.7 GB at 800k opens, and past 4.8 GB against the stock `echoserver` example at 1.5 M opens — see `results/canonical-run.log`.) - **CONTROL**: RSS flat at `4,208 KB`. Without the rekey window the replies are drained normally; TCP backpressure (the client never reads the failure replies) even throttles the flood. The control isolates the rekey window as the sole trigger. Both legs exercise the real server entry point (`Session::run` → `reply` → `server_read_encrypted`) over a real encrypted transport. Reproduce: `C=russh-lab N=800000 ./poc/run.sh` (see `poc/POC-README.md`). ## Remediation The priority queue carries locally generated channel-open replies, which are non-kex messages the server **must not send** during a rekey anyway (RFC 4253 §7.1). So the fix is to bound how much channel work is done during a rekey, not to drain the queue mid-rekey. Any of: - **(recommended, minimal)** cap the number of non-kex messages processed while a rekey is in progress and disconnect a peer that exceeds it. A stalled/abusive rekey is then torn down after a small constant instead of growing memory without bound. See `patch/rekey-message-cap.patch` (a ~10-line change local to `Session::run`, validated in the lab — the attack leg is disconnected after the cap and RSS stays flat; a normal rekey, which completes in one round trip, is unaffected). - bound / deferred-buffer channel messages during a rekey using the existing (currently dead) `pending_reads`/`pending_len` machinery, with a hard cap. - bound the number of in-flight channel opens per connection and reject beyond it. OpenSSH does not service new channels mid-kex; matching that intent closes the whole family.

Source published
30 Sep 2026 23:40:43 UTC
Source modified
1 Oct 2026 00:00:04 UTC
Observed here
24 Oct 2026 13:19:54 UTC
Source terms
GitHub Advisory Database · CC-BY-4.0

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · GitHub Advisory Database · CC-BY-4.0. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS_V3 · CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Publisher rating · MODERATE

ChangeIntel

An IT change radar: releases, security, known issues, retirements, documentation changes, and service status from public sources. Every item links to supporting evidence; dates and statuses can change after they are read.

Sources read 12 Nov 02:22 UTC · 274 of 275 readable · 272 fully read · documentation 469/469 current