{"id":"GHSA-93wv-jw9v-4972","summary":"Netty: HTTP/2 decompression leaks ByteBuf reference count when the decompressor channel is already closed (Direct memory leak / OOM DoS)","details":"### Summary\n\nA remote, unauthenticated peer can leak one direct `ByteBuf` per HTTP/2 `DATA` frame in\napplications that enable HTTP/2 content decompression via `DelegatingDecompressorFrameListener`.\nWhen a `DATA` frame is processed for a stream whose decompressor has already been closed,\n`Http2Decompressor.decompress(...)` retains the frame buffer but never releases it on the error\npath, so its reference count never returns to zero. Repeating this over a long-lived HTTP/2\nconnection exhausts direct memory and crashes the JVM with `OutOfMemoryError` — a denial of service.\n\n### Details\n\nIn `codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingDecompressorFrameListener.java`,\n`Http2Decompressor.decompress(...)` does:\n\n```java\n// around line 433\ndecompressor.writeInbound(data.retain());\n```\n\nThe argument `data.retain()` is evaluated **before** `writeInbound(...)` executes, incrementing the\nbuffer's reference count (`refCnt: 1 -\u003e 2`). The very first statement of\n`EmbeddedChannel.writeInbound(...)` is `ensureOpen()` (`EmbeddedChannel.java:360`), which throws\n`ClosedChannelException` when the decompressor's internal `EmbeddedChannel` has already been closed.\n\nWhen that happens:\n- the `DATA` payload has been `retain()`ed but never entered the pipeline, so the decoder's\n  `finally { release() }` never runs;\n- the surrounding `catch (Throwable t)` block in `decompress(...)` (around line 451) does **not**\n  release the extra reference;\n- the input buffer therefore can never reach refCnt 0, and its (typically direct) memory is leaked.\n\nThe decompressor channel is closed on a reachable path:\n`Http2Connection` `onStreamRemoved` → `Http2Decompressor.cleanup()` →\n`EmbeddedChannel.finishAndReleaseAll()`\n(`DelegatingDecompressorFrameListener.java:125-133` and `418-420`).\n\nA peer that sends `DATA` frames for a stream whose decompressor has already been cleaned up (e.g.\ncontinuing to send `DATA` after `END_STREAM` / stream removal) thus leaks one direct `ByteBuf` per\nframe.\n\n**Affected code**: `DelegatingDecompressorFrameListener.java`, method `Http2Decompressor.decompress(...)`\n— the `decompressor.writeInbound(data.retain())` call (line ~433) and its `catch (Throwable t)`\nblock (line ~451), which lacks a `data.release()` rollback.\n\n**Suggested fix**: track whether `writeInbound` succeeded and roll back the extra `retain()` only when\nthe data never entered the pipeline:\n\n```java\nboolean writeSucceeded = false;\ntry {\n    decompressor.writeInbound(data.retain());\n    writeSucceeded = true;            // pipeline now owns the release\n    if (endOfStream) {\n        decompressor.finish();\n    }\n    return 0;\n} catch (Throwable t) {\n    if (!writeSucceeded) {\n        data.release();               // roll back the extra retain(); data never entered pipeline\n    }\n    if (t instanceof Http2Exception) {\n        throw (Http2Exception) t;\n    }\n    throw streamError(stream.id(), INTERNAL_ERROR, t, ...);\n}\n```\n\n| Case | writeSucceeded | catch action | Reason |\n|------|:---:|---|---|\n| `ensureOpen()` throws (this bug) | `false` | `data.release()` | data never entered pipeline |\n| handler throws internally | `true` | no release | decoder `finally` already released |\n| `finish()` throws | `true` | no release | `writeInbound` already succeeded |\n\n### PoC\n\nReproduced against the official, unmodified `netty-codec-http2-4.2.15.Final.jar` from Maven Central,\nusing real netty classes and measuring `ByteBuf.refCnt()` directly (the leaking logic is not mocked).\n\nReproduction steps:\n\n1. Download the official artifacts and their dependencies from Maven Central (version `4.2.15.Final`):\n   `netty-common`, `netty-buffer`, `netty-transport`, `netty-resolver`, `netty-handler`,\n   `netty-codec-base`, `netty-codec`, `netty-codec-http`, `netty-codec-http2`,\n   `netty-codec-compression`.\n2. Build a real `Http2Decompressor` wrapping a real gzip decoder `EmbeddedChannel`\n   (`ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)`).\n3. Close the internal decompressor channel (equivalent to the end state of\n   `cleanup()` / `finishAndReleaseAll()`).\n4. Encode a real gzip `DATA` payload with `ZlibCodecFactory.newZlibEncoder(GZIP)` (`refCnt = 1`).\n5. Call `decompress(...)` on the closed channel.\n6. Observe: `writeInbound(...)` throws `ClosedChannelException` at its `ensureOpen()` entry\n   (`EmbeddedChannel.java:360`), reached from `DelegatingDecompressorFrameListener.java:433`;\n   `data.refCnt()` is now `2`.\n7. Release once as the frame reader would; `refCnt` stays at `1` (`release()` returns `false`) → leaked.\n\nObserved reference-count trace:\n\n```\ngzipData initial refCnt = 1\ndecompress -\u003e data.retain()        -\u003e refCnt = 2   (retain applied, never rolled back)\ncaller releases once               -\u003e refCnt = 1   (release() returns false; not deallocated)\n=\u003e buffer never reaches 0 -\u003e direct memory leaked\n```\n\nObserved exception stack (confirms the leak point):\n\n```\njava.nio.channels.ClosedChannelException\n    at io.netty.channel.embedded.EmbeddedChannel.checkOpen(EmbeddedChannel.java:959)\n    at io.netty.channel.embedded.EmbeddedChannel.ensureOpen(EmbeddedChannel.java:979)\n    at io.netty.channel.embedded.EmbeddedChannel.writeInbound(EmbeddedChannel.java:360)\n    at io.netty.handler.codec.http2.DelegatingDecompressorFrameListener$Http2Decompressor\n            .decompress(DelegatingDecompressorFrameListener.java:433)\n```\n\nTwo notes on the harness (they do not affect the leak mechanism):\n- The internal channel is closed directly via `close()` rather than through `cleanup()`. The end\n  state is identical (channel closed → `writeInbound` throws at `ensureOpen()`); the bug depends on\n  \"channel closed → retain not rolled back\", not on *how* the channel was closed.\n- In the isolated harness the rethrown `StreamException`'s root cause shows as `NullPointerException`\n  because the harness does not initialise an `Http2LocalFlowController` (a secondary exception\n  reported during channel close). The leak is already sealed at the `ClosedChannelException` thrown\n  by `writeInbound`'s `ensureOpen()` (line 360); in a real server with the flow controller\n  initialised, the triggering exception is the `ClosedChannelException` itself.\n\nA complete self-contained PoC (`Verify02DecompressLeak.java`, ~150 lines, no test framework) plus the\nexact `javac` / `java` commands can be attached on request.\n\n### Impact\n\n- **Vulnerability type**: uncontrolled resource consumption / memory leak (CWE-401), leading to\n  denial of service. Each crafted `DATA` frame leaks one (typically direct/off-heap) `ByteBuf`.\n- **Who is impacted**: any server (or client) that enables HTTP/2 content decompression by installing\n  `DelegatingDecompressorFrameListener` in its HTTP/2 pipeline.\n- **Attacker requirements**: remote, unauthenticated. The attacker only needs to send HTTP/2 `DATA`\n  frames for a stream whose decompressor has been cleaned up (e.g. continue sending `DATA` after\n  `END_STREAM`). No special server configuration beyond decompression being enabled.\n- **Result**: sustained triggering over a long-lived connection exhausts direct memory and crashes\n  the JVM with `OutOfMemoryError`.","aliases":["CVE-2026-56819"],"modified":"2026-07-31T17:00:12.194102762Z","published":"2026-07-31T16:51:50Z","database_specific":{"cwe_ids":["CWE-400"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-07-31T16:51:50Z","nvd_published_at":"2026-07-21T23:17:52Z"},"references":[{"type":"WEB","url":"https://github.com/netty/netty/security/advisories/GHSA-93wv-jw9v-4972"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-56819"},{"type":"WEB","url":"https://github.com/netty/netty/commit/5b68c61f37aa4a3045cba624cbea239655c9003b"},{"type":"WEB","url":"https://github.com/netty/netty/commit/bb2ff68a1fb71cb4b0eb9a9e17b66c52aff680c6"},{"type":"PACKAGE","url":"https://github.com/netty/netty"},{"type":"WEB","url":"https://github.com/netty/netty/releases/tag/netty-4.1.136.Final"},{"type":"WEB","url":"https://github.com/netty/netty/releases/tag/netty-4.2.16.Final"}],"affected":[{"package":{"name":"io.netty:netty-codec-http2","ecosystem":"Maven","purl":"pkg:maven/io.netty/netty-codec-http2"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.2.0"},{"fixed":"4.2.16.Final"}]}],"versions":["4.2.0.Final","4.2.1.Final","4.2.10.Final","4.2.11.Final","4.2.12.Final","4.2.13.Final","4.2.14.Final","4.2.15.Final","4.2.2.Final","4.2.3.Final","4.2.4.Final","4.2.5.Final","4.2.6.Final","4.2.7.Final","4.2.8.Final","4.2.9.Final"],"database_specific":{"last_known_affected_version_range":"\u003c= 4.2.15.Final","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-93wv-jw9v-4972/GHSA-93wv-jw9v-4972.json"}},{"package":{"name":"io.netty:netty-codec-http2","ecosystem":"Maven","purl":"pkg:maven/io.netty/netty-codec-http2"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.1.0.Final"},{"fixed":"4.1.136.Final"}]}],"versions":["4.1.0.Final","4.1.1.Final","4.1.10.Final","4.1.100.Final","4.1.101.Final","4.1.102.Final","4.1.103.Final","4.1.104.Final","4.1.105.Final","4.1.106.Final","4.1.107.Final","4.1.108.Final","4.1.109.Final","4.1.11.Final","4.1.110.Final","4.1.111.Final","4.1.112.Final","4.1.113.Final","4.1.114.Final","4.1.115.Final","4.1.116.Final","4.1.117.Final","4.1.118.Final","4.1.119.Final","4.1.12.Final","4.1.120.Final","4.1.121.Final","4.1.122.Final","4.1.123.Final","4.1.124.Final","4.1.125.Final","4.1.126.Final","4.1.127.Final","4.1.128.Final","4.1.129.Final","4.1.13.Final","4.1.130.Final","4.1.131.Final","4.1.132.Final","4.1.133.Final","4.1.134.Final","4.1.135.Final","4.1.14.Final","4.1.15.Final","4.1.16.Final","4.1.17.Final","4.1.18.Final","4.1.19.Final","4.1.2.Final","4.1.20.Final","4.1.21.Final","4.1.22.Final","4.1.23.Final","4.1.24.Final","4.1.25.Final","4.1.26.Final","4.1.27.Final","4.1.28.Final","4.1.29.Final","4.1.3.Final","4.1.30.Final","4.1.31.Final","4.1.32.Final","4.1.33.Final","4.1.34.Final","4.1.35.Final","4.1.36.Final","4.1.37.Final","4.1.38.Final","4.1.39.Final","4.1.4.Final","4.1.40.Final","4.1.41.Final","4.1.42.Final","4.1.43.Final","4.1.44.Final","4.1.45.Final","4.1.46.Final","4.1.47.Final","4.1.48.Final","4.1.49.Final","4.1.5.Final","4.1.50.Final","4.1.51.Final","4.1.52.Final","4.1.53.Final","4.1.54.Final","4.1.55.Final","4.1.56.Final","4.1.57.Final","4.1.58.Final","4.1.59.Final","4.1.6.Final","4.1.60.Final","4.1.61.Final","4.1.62.Final","4.1.63.Final","4.1.64.Final","4.1.65.Final","4.1.66.Final","4.1.67.Final","4.1.68.Final","4.1.69.Final","4.1.7.Final","4.1.70.Final","4.1.71.Final","4.1.72.Final","4.1.73.Final","4.1.74.Final","4.1.75.Final","4.1.76.Final","4.1.77.Final","4.1.78.Final","4.1.79.Final","4.1.8.Final","4.1.80.Final","4.1.81.Final","4.1.82.Final","4.1.83.Final","4.1.84.Final","4.1.85.Final","4.1.86.Final","4.1.87.Final","4.1.88.Final","4.1.89.Final","4.1.9.Final","4.1.90.Final","4.1.91.Final","4.1.92.Final","4.1.93.Final","4.1.94.Final","4.1.95.Final","4.1.96.Final","4.1.97.Final","4.1.98.Final","4.1.99.Final"],"database_specific":{"last_known_affected_version_range":"\u003c= 4.1.135.Final","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-93wv-jw9v-4972/GHSA-93wv-jw9v-4972.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"}]}