SaibaRadar

CVE-2026-116774 — 脆弱性インテリジェンス

— russh

公表日:

CVSS
4.3
CISA KEV
未掲載(悪用がないことを意味しません)

情報源の原文(翻訳ではありません)

russh: peer-declared zero maximum packet size permanently stalls channel writes (silent AsyncWrite hang) ## 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 o

情報源