russh: peer-declared zero maximum packet size permanently stalls channel writes (silent AsyncWrite hang)
Collected exploitation and prediction evidence
Risk source coverageCISA 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-116774
Publishedrussh is a Rust SSH client and server library that includes an SSH agent protocol implementation. From 0.60.0 until 0.65.0, channel-open parsing accepts a peer-declared maximum_packet_size of zero and passes it into ChannelTx::poll_writable in russh/src/channels/io/tx.rs. The Channel::data, Channel::make_writer, and Channel::into_stream asynchronous write paths then calculate a writable size of zero and remain pending forever because window adjustments cannot satisfy a static zero packet maximum. An authenticated client can strand server writes, and a malicious server can strand client writes, allowing affected tasks, channels, and connections to accumulate without surfacing an error. This issue is fixed in version 0.65.0.
- Source published
- 27 Oct 2026 20:25:26 UTC
- Source modified
- 28 Oct 2026 16:13:18 UTC
- Observed here
- 28 Oct 2026 17:46:48 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 · 4.3 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L
euvd · EUVD-2026-111389
Publishedrussh is a Rust SSH client and server library that includes an SSH agent protocol implementation. From 0.60.0 until 0.65.0, channel-open parsing accepts a peer-declared maximum_packet_size of zero and passes it into ChannelTx::poll_writable in russh/src/channels/io/tx.rs. The Channel::data, Channel::make_writer, and Channel::into_stream asynchronous write paths then calculate a writable size of zero and remain pending forever because window adjustments cannot satisfy a static zero packet maximum. An authenticated client can strand server writes, and a malicious server can strand client writes, allowing affected tasks, channels, and connections to accumulate without surfacing an error. This issue is fixed in version 0.65.0.
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: Oct 27, 2026, 8:25:26 PM
dateUpdated: Oct 28, 2026, 4:13:18 PM
- Source published
- Oct 27, 2026, 8:25:26 PM (timezone not stated)
- Source modified
- Oct 28, 2026, 4:13:18 PM (timezone not stated)
- Observed here
- 30 Oct 2026 23:48:09 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 · 4.3 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L
nvd · CVE-2026-116774
Publishedrussh is a Rust SSH client and server library that includes an SSH agent protocol implementation. From 0.60.0 until 0.65.0, channel-open parsing accepts a peer-declared maximum_packet_size of zero and passes it into ChannelTx::poll_writable in russh/src/channels/io/tx.rs. The Channel::data, Channel::make_writer, and Channel::into_stream asynchronous write paths then calculate a writable size of zero and remain pending forever because window adjustments cannot satisfy a static zero packet maximum. An authenticated client can strand server writes, and a malicious server can strand client writes, allowing affected tasks, channels, and connections to accumulate without surfacing an error. This issue is fixed in version 0.65.0.
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-835
Original publisher date fields
published: 2026-10-27T21:18:05.933
lastModified: 2026-10-28T17:18:06.990
- Source published
- 27 Oct 2026 21:18:05 UTC
- Source modified
- 28 Oct 2026 17:18:06 UTC
- Observed here
- 30 Oct 2026 23:47:40 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 · 4.3 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L
osv · GHSA-fw5c-8223-6h42
Published## Summary A peer can advertise `maximum packet size = 0` in its channel-open message (`CHANNEL_OPEN` when attacking a server, `CHANNEL_OPEN_CONFIRMATION` when attacking a client). russh accepts the value without validation and copies it into the channel write state, where it caps the size of every outgoing data chunk. In the `AsyncWrite` code path (`ChannelTx::poll_writable`, used by `Channel::data()`, `Channel::make_writer()` and `Channel::into_stream()`), a cap of 0 makes every write future stay `Poll::Pending` forever: `writable = max_packet_size.min(window_size).min(buf_len)` is always 0, and no future event can ever raise it (a static maximum packet size of 0 can never be satisfied by any amount of peer-granted window). The write neither completes nor errors, so the application task hangs silently. Every other write path in russh already treats `max_packet_size == 0` as an error (`Error::Inconsistent`): `Channel::reserve_writable_chunk` (`russh/src/channels/mod.rs:363-365`) and the session packetizers (`russh/src/session.rs:489-492` and `549-552`). The `AsyncWrite` path is the only one that lacks the guard - it contradicts the crate's own invariant. ## Root cause (1) Peer value accepted without validation: - `russh/src/parsing.rs:44` (`OpenChannelMessage::parse`): `let maxpacket = map_err!(u32::decode(r))?;` stored as `recipient_maximum_packet_size` (`:25-30`); no range check. Same for `ChannelOpenConfirmation::decode` at `russh/src/parsing.rs:194-207`. - The only packet-size validation anywhere is an upper bound on the *local* config that merely logs (`russh/src/client/mod.rs:1189-1194`, `russh/src/server/mod.rs:912-917`). Nothing validates the peer's value - 0 passes silently. (2) The value flows into the channel write state verbatim: - Server: `russh/src/server/encrypted.rs:1877-1900` - `ChannelParams { recipient_maximum_packet_size: msg.recipient_maximum_packet_size, .. }`, then `Channel::new(..., channel_params.recipient_maximum_packet_size, ...)`. - Client: `russh/src/client/encrypted.rs:471-488` - `parameters.confirm(&msg)` (`russh/src/lib_inner.rs:519-523`) and `ChannelMsg::Open { max_packet_size: msg.maximum_packet_size, .. }`, consumed by `wait_channel_confirmation` (`russh/src/client/mod.rs:698-722`), which builds `ChannelWriteHalf { max_packet_size, .. }`. - The write half hands the value to the writer: `make_writer_ext` (`russh/src/channels/mod.rs:433-441`) and `into_stream` (`russh/src/channels/mod.rs:661-670`) pass `self.max_packet_size` to `ChannelTx::new`. (3) The hang - `russh/src/channels/io/tx.rs:82-108` (current v0.64.1, byte-identical in v0.60.0): ```rust fn poll_writable(&mut self, cx: &mut Context<'_>, buf_len: usize) -> Poll<NonZeroUsize> { ... let writable = (self.max_packet_size).min(*window_size).min(buf_len as u32) as usize; match NonZeroUsize::try_from(writable) { Ok(w) => { *window_size -= writable as u32; if *window_size > 0 { self.notify.notify_one(); } Poll::Ready(w) } Err(_) => { drop(window_size); ready!(self.window_size_notication.poll_unpin(cx)); self.window_size_notication = WatchNotification::new(Arc::clone(&self.notify)); cx.waker().wake_by_ref(); Poll::Pending } } } ``` With `max_packet_size == 0`, `writable` is always 0 regardless of `window_size` and `buf_len`; the task parks on the window notification, gets re-polled on every `CHANNEL_WINDOW_ADJUST`, recomputes 0, and parks again - forever. (4) The guard exists everywhere else: ```rust // russh/src/channels/mod.rs:363-365 (used by send_bytes -> data_bytes / extended_data_bytes) if self.max_packet_size == 0 { return Err(Error::Inconsistent); } // russh/src/session.rs:490-492 and :550-552 (packetizer) if max_packet_size == 0 { return Err(crate::Error::Inconsistent); } ``` Note that `Channel::data(reader)` (`russh/src/channels/mod.rs:321-323`) goes through `send_data` -> `tokio::io::copy(&mut data, &mut tx)` (`:350-360`) into the same `ChannelTx`, so the primary documented data API hangs as well; only the `*_bytes` variants reach the guard. ## Attack scenarios - **Server (authenticated remote DoS)**: with the default configuration, an authenticated client opens a session channel advertising `maximum packet size = 0`. Whenever the server application writes on that channel through `AsyncWrite` (echo, shell prompts, SFTP - russh's own examples echo via `into_stream()`), the write future never completes. The handler task is stranded and no error surfaces, so application cleanup paths do not trigger. Repeat per channel and per connection; keep-alive traffic resets the server `inactivity_timeout` (default 600 s), so stranded resources persist as long as the attacker wants. - **Client (malicious server)**: a malicious or buggy server confirms an opened channel with `maximum packet size = 0`; the client application's writes on that channel stall forever. The client `inactivity_timeout` defaults to `None`, so nothing ever tears the session down. - This is distinct from a `window_size == 0` stall: a zero window is dynamic flow control that a well-behaved peer releases as soon as its buffers drain, while `maximum packet size` is a static peer declaration where 0 can never be satisfied by any future packet. RFC 4254 section 5.1 recommends a minimum of 32768 - legitimate peers do not send 0. ## Impact Permanent, silent denial of service for the affected writes, with stranded tasks/channels/connections that accumulate for the attacker - resource exhaustion of a server or of a client application. Because the failure mode is `Poll::Pending` instead of `Err`, ordinary application mitigations (error handling, timeouts on writes, connection teardown) do not engage on their own. ## Reproducer Two stock russh instances are enough (no raw SSH crafting required): 1. Server: default config plus a host key; `channel_open_session` accepts and echoes using `tokio::io::copy` into `channel.into_stream()` (the pattern from russh's own examples). 2. Client: default config except `config.maximum_packet_size = 0` (accepted silently; see above), then `authenticate_publickey`, `channel_open_session().await`, `channel.data(&b"hello"[..]).await`. The client's `CHANNEL_OPEN` goes out with `maximum packet size = 0` (`russh/src/client/session.rs:21-44` encodes `self.common.config.maximum_packet_size` verbatim). All three scenarios below were verified end-to-end against a stock russh v0.64.1 build (ring backend), default settings otherwise: - control (both sides default, 32768): the server echoes and the client receives `hello`; - client `maximum_packet_size = 0`: the server prints the received bytes and then the echo write never completes - no data, no error, the client times out; - server `maximum_packet_size = 0`: the client's `channel.data(...)` future never completes - no error, it times out. Observed output (abridged): ``` === SCENARIO A (control): both sides default (maxpacket 32768) === [server] read 5 bytes; echoing via AsyncWrite ... [server] echo write completed RESULT A: echo received: "hello" === SCENARIO B: client advertises maximum_packet_size = 0 === [server] read 5 bytes; echoing via AsyncWrite ... RESULT B: TIMEOUT after 6s - server-side echo write is permanently stalled (no error, no data) === SCENARIO C: server advertises maximum_packet_size = 0 === RESULT C: TIMEOUT after 6s - client-side channel.data() is permanently stalled ``` The server task stays parked inside `ChannelTx::poll_writable`; nothing is logged and nothing is returned to the handler. The harness source (server: `into_stream()` echo; client: one config toggle) can be attached on request. Unit-level sketch of the core defect: ```rust let mut tx = ChannelTx::<Msg>::new( sender, ChannelId(0), Arc::new(Mutex::new(4096)), // window_size Arc::new(Notify::new()), // notify 0, // max_packet_size None, ); // poll_write(&mut tx, cx, b"x") stays Poll::Pending forever, // even after CHANNEL_WINDOW_ADJUST raises `window_size`. ``` ## Suggested fix - **Validate at parse time (preferred)**: reject `maximum_packet_size == 0` with `Error::Inconsistent` in `OpenChannelMessage::parse` and `ChannelOpenConfirmation::decode` (`russh/src/parsing.rs`), consistent with the existing guards; optionally clamp any value below the RFC 4254 recommended minimum (32768) instead. - **Defense in depth**: in `ChannelTx::poll_writable` (`russh/src/channels/io/tx.rs:82`), when `self.max_packet_size == 0`, surface an `io::Error` from `poll_write` instead of returning `Poll::Pending`. Both match the treatment PR #695 already applied to the other write paths in v0.61.0 (the `reserve_writable_chunk` and session packetizer guards). ## Affected versions Verified by inspecting the release artifacts: **0.60.0 through 0.64.1 (current) are affected** - `poll_writable` is byte-identical in v0.60.0 and the peer-value plumbing is present in all of them. The `ChannelTx` write logic and the peer-value flow predate 0.60 (they go back to the `channels::io` refactor), so we recommend treating all releases that ship `channels::io::ChannelTx` as affected. The zero-guards in the other write paths were only added in 0.61.0, so on 0.60.x those paths do not even fail with an error. ## Reporter Tshellauth
- Source published
- 30 Oct 2026 22:58:53 UTC
- Source modified
- 30 Oct 2026 23:15:05 UTC
- Observed here
- 30 Oct 2026 23:47:35 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:L
Publisher rating · MODERATE