KEVWire

Security intelligence, with context.

Response snapshot Oct 31, 2026, 01:26 UTC

Back to the wire
Permalink
VulnerabilityCVE-2026-116774

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

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).

At a glance

Exploited
Not known
Severity
4.3 Medium (CVSS 3.1)
Chance of exploitation
Not estimated
Sources
1 source

What to do

GitHub lists first patched versions: russh: 0.65. Confirm the affected range and upgrade guidance in the exact advisory.

Read GitHub-reviewed advisory GHSA-fw5c-8223-6h42 (opens a new tab)

What happened

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.

Continue reading

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):

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:

// 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_pa

Affected package ranges reported by GitHub: russh: >= 0.60.0, <= 0.64.1

Scores and how they were measured

CVSS base severity

4.3

Version 3.1As of

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

Measurement source (opens a new tab)

How bad it is if exploited. Not a prediction of exploitation or proof you are exposed.

EPSS probability

Not available

A model's estimate of exploitation in the next 30 days, not certainty or your own risk.

Related technology

Sources

  1. Primary source

    GitHub-reviewed advisory GHSA-fw5c-8223-6h42 (opens a new tab)

    The public advisory supplies the description, affected package/version ranges, and any listed CVSS metric. GitHub review is not local review.

    Published Checked

  2. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-fw5c-8223-6h42, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

Cite this record

For factual claims, cite the primary sources above; cite KEVWire for its compilation, dates, and classifications.

APA

KEVWire. (2026, October 30). russh: peer-declared zero maximum packet size permanently stalls channel writes (silent AsyncWrite hang) (CVE-2026-116774). https://kevwire.com/record/CVE-2026-116774

MLA

"russh: peer-declared zero maximum packet size permanently stalls channel writes (silent AsyncWrite hang) (CVE-2026-116774)." KEVWire, 30 Oct. 2026, kevwire.com/record/CVE-2026-116774.

Chicago

KEVWire. "russh: peer-declared zero maximum packet size permanently stalls channel writes (silent AsyncWrite hang) (CVE-2026-116774)." Last modified October 30, 2026. https://kevwire.com/record/CVE-2026-116774.

Evidence summary (plain text)

russh: peer-declared zero maximum packet size permanently stalls channel writes (silent AsyncWrite hang) (Oct 30, 2026).
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.
CVSS 4.3 (version 3.1; evidence as of Oct 30, 2026; https://github.com/advisories/GHSA-fw5c-8223-6h42).
Sources:
GitHub-reviewed advisory GHSA-fw5c-8223-6h42: https://github.com/advisories/GHSA-fw5c-8223-6h42
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc
Record ID: CVE-2026-116774.
Record origin: Ingested record.
AI evidence: Not assessed.
Known exploitation: Not assessed.
Zero-day status: Not assessed. KEV membership alone does not establish zero-day status.
About this data

This record was ingested from a data source. Retrieved: Oct 31, 2026, 00:50 UTC. Ingestion is not an independent editorial investigation.

A CISA KEV listing documents known exploitation, not necessarily a zero-day; absence from the catalog does not prove a vulnerability has never been exploited.

Technology tags indicate possible relevance, not verified exposure. Confirm the affected product, version, and configuration against your own inventory; KEVWire does not scan your systems.

A personal reading checklist on this device, not verified remediation.

Record JSON

Your account

Email alerts

Email alerts are coming as part of KEVWire Premium: join the waitlist. To follow your stack today, use its RSS feed, which honors your filters, or connect an AI agent through the API.

Suggest an improvement

Ideas, missing sources, corrections, and bugs are all welcome. No account needed. Suggestions are reviewed by a person before anything changes.

Record details

Record detail