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: 272/272 feeds/APIs · 469/469 docs · 8/9 advisories · checked 02:23 UTC Customize Public modeDiscuss ChangeIntel on Discord

russh: peer-declared zero maximum packet size permanently stalls channel writes (silent AsyncWrite hang)

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-116774

Published

russh 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

Published

russh 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

Published

russh 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

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 31 Oct 02:23 UTC · 272 of 272 readable · 270 fully read · documentation 469/469 current