Yamux remote Panic via malformed WindowUpdate credit
Description
Yamux is a stream multiplexer over reliable, ordered connections such as TCP/IP. From 0.13.0 to before 0.13.9, a specially crafted WindowUpdate can cause arithmetic overflow in send-window accounting, which triggers a panic in the connection state machine. This is remotely reachable over a normal network connection and does not require authentication. This vulnerability is fixed in 0.13.9.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Oversized WindowUpdate frames cause an arithmetic overflow in Yamux stream multiplexer, leading to a remotely exploitable panic before v0.13.9.
Vulnerability
Overview
Yamux, a stream multiplexer for reliable ordered connections like TCP/IP, contains an arithmetic overflow vulnerability in its send-window accounting logic. Versions 0.13.0 through 0.13.8 are affected. The bug is triggered by a specially crafted WindowUpdate frame; when processing such a frame, the internal send-window tracking can overflow, causing the connection state machine to panic [1][2].
Exploitation
An attacker can exploit this vulnerability over a normal network connection without any authentication. The attack is remotely reachable—no special privileges or prior access are required. The malicious actor simply sends a crafted WindowUpdate frame to an open Yamux connection, which is typically used in peer-to-peer networking scenarios such as libp2p [2][4].
Impact
When the panic occurs, the affected Yamux connection is abruptly terminated. This results in a denial of service for any application relying on that multiplexed stream connection. Since the panic happens in the connection state machine, it may also disrupt other streams multiplexed over the same connection, leading to cascading failures in the application [1][2].
Mitigation
The vulnerability is fixed in version 0.13.9. The fix improves flow-control credit verification for window updates, ensuring that arithmetic overflow is prevented before it can cause a panic [1][2][3]. Users should update to the patched version immediately. No workarounds have been publicly documented beyond applying the update [2].
- feat: improve flow-control credit verification for window updates by jxs · Pull Request #221 · libp2p/rust-yamux
- NVD - CVE-2026-31814
- feat: improve flow-control credit verification for window updates (#221) · libp2p/rust-yamux@b1aae09
- GitHub - libp2p/rust-yamux: Multiplexer over reliable, ordered connections.
AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
yamuxcrates.io | >= 0.13.0, < 0.13.9 | 0.13.9 |
Affected products
2- libp2p/rust-yamuxv5Range: >= 0.13.0, < 0.13.9
Patches
1b1aae09d60c0feat: improve flow-control credit verification for window updates (#221)
4 files changed · +49 −40
yamux/src/connection.rs+22 −12 modified@@ -615,14 +615,6 @@ impl<T: AsyncRead + AsyncWrite + Unpin> Active<T> { log::error!("{}: invalid stream id {}", self.id, stream_id); return Action::Terminate(Frame::protocol_error()); } - if frame.body().len() > DEFAULT_CREDIT as usize { - log::error!( - "{}/{}: 1st body of stream exceeds default credit", - self.id, - stream_id - ); - return Action::Terminate(Frame::protocol_error()); - } if self.streams.contains_key(&stream_id) { log::error!("{}/{}: stream already exists", self.id, stream_id); return Action::Terminate(Frame::protocol_error()); @@ -637,7 +629,15 @@ impl<T: AsyncRead + AsyncWrite + Unpin> Active<T> { if is_finish { shared.update_state(self.id, stream_id, State::RecvClosed); } - shared.consume_receive_window(frame.body_len()); + if let Err(_err) = shared.consume_receive_window(frame.body_len()) { + log::error!( + "{}/{}: 1st body of stream exceeds default credit", + self.id, + stream_id + ); + + return Action::Terminate(Frame::protocol_error()); + } shared.buffer.push(frame.into_body()); } self.streams.insert(stream_id, stream.clone_shared()); @@ -646,18 +646,21 @@ impl<T: AsyncRead + AsyncWrite + Unpin> Active<T> { if let Some(s) = self.streams.get_mut(&stream_id) { let mut shared = s.lock(); - if frame.body_len() > shared.receive_window() { + + if let Err(_err) = shared.consume_receive_window(frame.body_len()) { log::error!( "{}/{}: frame body larger than window of stream", self.id, stream_id ); + return Action::Terminate(Frame::protocol_error()); } + if is_finish { shared.update_state(self.id, stream_id, State::RecvClosed); } - shared.consume_receive_window(frame.body_len()); + shared.buffer.push(frame.into_body()); if let Some(w) = shared.reader.take() { w.wake() @@ -730,7 +733,14 @@ impl<T: AsyncRead + AsyncWrite + Unpin> Active<T> { if let Some(s) = self.streams.get_mut(&stream_id) { let mut shared = s.lock(); - shared.increase_send_window_by(frame.header().credit()); + if let Err(err) = shared.increase_send_window_by(frame.header().credit()) { + log::error!( + "{}/{}: could not increase the send window, {err}", + self.id, + stream_id + ); + return Action::Terminate(Frame::protocol_error()); + } if is_finish { shared.update_state(self.id, stream_id, State::RecvClosed);
yamux/src/connection/stream/flow_control.rs+10 −11 modified@@ -3,7 +3,7 @@ use std::{cmp, sync::Arc}; use parking_lot::Mutex; use web_time::Instant; -use crate::{connection::rtt::Rtt, Config, DEFAULT_CREDIT}; +use crate::{connection::rtt::Rtt, Config, ConnectionError, DEFAULT_CREDIT}; #[derive(Debug)] pub(crate) struct FlowController { @@ -169,29 +169,28 @@ impl FlowController { self.send_window } - pub(crate) fn consume_send_window(&mut self, i: u32) { + pub(crate) fn consume_send_window(&mut self, i: u32) -> Result<(), ConnectionError> { self.send_window = self .send_window .checked_sub(i) - .expect("not exceed send window"); + .ok_or(ConnectionError::InvalidWindowUpdate)?; + Ok(()) } - pub(crate) fn increase_send_window_by(&mut self, i: u32) { + pub(crate) fn increase_send_window_by(&mut self, i: u32) -> Result<(), ConnectionError> { self.send_window = self .send_window .checked_add(i) - .expect("send window not to exceed u32"); + .ok_or(ConnectionError::InvalidWindowUpdate)?; + Ok(()) } - pub(crate) fn receive_window(&self) -> u32 { - self.receive_window - } - - pub(crate) fn consume_receive_window(&mut self, i: u32) { + pub(crate) fn consume_receive_window(&mut self, i: u32) -> Result<(), ConnectionError> { self.receive_window = self .receive_window .checked_sub(i) - .expect("not exceed receive window"); + .ok_or(ConnectionError::InvalidWindowUpdate)?; + Ok(()) } }
yamux/src/connection/stream.rs+10 −17 modified@@ -10,6 +10,7 @@ use crate::connection::rtt::Rtt; use crate::frame::header::ACK; +use crate::ConnectionError; use crate::{ chunks::Chunks, connection::{self, rtt, StreamCommand}, @@ -372,16 +373,12 @@ impl AsyncWrite for Stream { shared.writer = Some(cx.waker().clone()); return Poll::Pending; } - let k = std::cmp::min( - shared.send_window(), - buf.len().try_into().unwrap_or(u32::MAX), - ); - let k = std::cmp::min( - k, - self.config.split_send_size.try_into().unwrap_or(u32::MAX), - ); - shared.consume_send_window(k); - Vec::from(&buf[..k as usize]) + let k = std::cmp::min(shared.send_window() as usize, buf.len()); + let k = std::cmp::min(k, self.config.split_send_size); + shared + .consume_send_window(k as u32) + .expect("not exceed receive window"); + Vec::from(&buf[..k]) }; let n = body.len(); let mut frame = Frame::data(self.id, body).expect("body <= u32::MAX").left(); @@ -527,19 +524,15 @@ impl Shared { self.flow_controller.send_window() } - pub(crate) fn consume_send_window(&mut self, i: u32) { + pub(crate) fn consume_send_window(&mut self, i: u32) -> Result<(), ConnectionError> { self.flow_controller.consume_send_window(i) } - pub(crate) fn increase_send_window_by(&mut self, i: u32) { + pub(crate) fn increase_send_window_by(&mut self, i: u32) -> Result<(), ConnectionError> { self.flow_controller.increase_send_window_by(i) } - pub(crate) fn receive_window(&self) -> u32 { - self.flow_controller.receive_window() - } - - pub(crate) fn consume_receive_window(&mut self, i: u32) { + pub(crate) fn consume_receive_window(&mut self, i: u32) -> Result<(), ConnectionError> { self.flow_controller.consume_receive_window(i) } }
yamux/src/error.rs+7 −0 modified@@ -24,6 +24,9 @@ pub enum ConnectionError { Closed, /// Too many streams are open, so no further ones can be opened at this time. TooManyStreams, + /// A window update operation was rejected because the supplied credit + /// is invalid for the current flow-control window (e.g. overflow). + InvalidWindowUpdate, } impl std::fmt::Display for ConnectionError { @@ -36,6 +39,9 @@ impl std::fmt::Display for ConnectionError { } ConnectionError::Closed => f.write_str("connection is closed"), ConnectionError::TooManyStreams => f.write_str("maximum number of streams reached"), + ConnectionError::InvalidWindowUpdate => { + f.write_str("invalid window update for the current flow control window") + } } } } @@ -48,6 +54,7 @@ impl std::error::Error for ConnectionError { ConnectionError::NoMoreStreamIds | ConnectionError::Closed | ConnectionError::TooManyStreams => None, + ConnectionError::InvalidWindowUpdate => None, } } }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-4w32-2493-32g7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-31814ghsaADVISORY
- github.com/libp2p/rust-yamux/commit/b1aae09d60c0bd6a5915a5448f4e8cbc5174db53ghsaWEB
- github.com/libp2p/rust-yamux/pull/221ghsaWEB
- github.com/libp2p/rust-yamux/releases/tag/yamux-v0.13.9ghsaWEB
- github.com/libp2p/rust-yamux/security/advisories/GHSA-4w32-2493-32g7ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.