GitHub Advisoryadvisory · crates.io

GHSA-fw5c-8223-6h42 russh: peer-declared zero maximum packet size permanently stalls channel writes (silent AsyncWrite hang)

MEDIUM Published 2026-10-30 Modified 2026-10-30 Official page ↗
Aliases
CVE-2026-116774
Ecosystem
crates.io
Packages
russh
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L
Weaknesses
CWE-400, CWE-835
Reviewed
2026-10-30

Affected packages

• crates.io russh: >= 0.60.0, < 0.65 → fixed in 0.65

Description

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 { ... 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::::new( sender, ChannelId(0), Arc::new(Mutex::new(4096)), // window_size Arc::new(Notify::new()), // notify 0, …

Related

Alias · CVE (1)

  • CVE-2026-116774 russh is a Rust SSH client and server library that includes an SSH agent protocol implementation. vulnerability

Weakness · CWE (2)

  • CWE-400 Uncontrolled Resource Consumption weakness
  • CWE-835 Loop with Unreachable Exit Condition weakness

References