Fedora Account System
Red Hat Associate
Red Hat Customer
ByteBuf Leak in StompSubframeDecoder When a Frame Body Is Never Terminated A public GitHub Security Advisory (GHSA-ghg5-c4jg-8q5j) describes the following issue: ## Summary `StompSubframeDecoder` allocates a chunk buffer from the channel allocator once a frame's declared `content-length` has been satisfied, and parks it in an instance field while it waits for the single NUL byte that terminates the frame. If that byte never arrives, nothing ever releases the buffer. The decoder overrides neither `handlerRemoved0` nor `channelInactive`, so the buffer survives the connection that created it. A remote peer leaks one allocator buffer per connection by sending a complete, well-formed frame body and simply not sending its terminating byte. With the default pooled allocator the memory is never returned to the pool, so it is not reclaimed when the peer disconnects and not reclaimed by garbage collection. The leak accumulates for the lifetime of the process. ## Details In `codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeDecoder.java`: ```java case READ_CONTENT: ... if (contentLength >= 0) { int remainingLength = (int) (contentLength - alreadyReadChunkSize); if (toRead > remainingLength) { toRead = remainingLength; } ByteBuf chunkBuffer = readBytes(ctx.alloc(), in, toRead); // allocated if ((alreadyReadChunkSize += toRead) >= contentLength) { lastContent = new DefaultLastStompContentSubframe(chunkBuffer); // parked in a field checkpoint(State.FINALIZE_FRAME_READ); } ... // Fall through. case FINALIZE_FRAME_READ: skipNullCharacter(in); // needs one more byte ... out.add(lastContent); resetDecoder(); ``` The class is a `ReplayingDecoder`. When the body is complete but the trailing NUL has not arrived, `skipNullCharacter(in)` throws a replay `Signal` so the decoder can be re-entered with more input. `lastContent` is left holding the allocated buffer, which is correct while the connection is alive and more data may still arrive. The problem is what happens if more data never arrives. Three things combine: `Signal` extends `Error`, not `Exception`. The `catch (Exception e)` block in `decode(..)` releases `lastContent` on failure, but a replay signal is not an `Exception`, so that path does not run. `resetDecoder()` sets `lastContent = null` without releasing it, which is correct on the success path because ownership has already passed to `out` at that point, but it means the field is never a release site. `StompSubframeDecoder` overrides neither `handlerRemoved0` nor `channelInactive`. `ByteToMessageDecoder.handlerRemoved` releases the cumulation buffer, but knows nothing about this decoder's own field, so nothing releases `lastContent` at teardown. The result is that between "body complete" and "NUL received" the decoder holds an allocator buffer with no release path other than the arrival of one specific byte that the peer controls. ## Proof of concept `PocStompLeak.java` drives the released `io.netty:netty-codec-stomp:4.2.17.Final` through `EmbeddedChannel` with a counting allocator that records every buffer the channel allocates, then reports which are still referenced after the channel is closed. Build and run with the attached `pom.xml`: ``` docker run --rm -v "$PWD:/work" -w /work maven:3-eclipse-temurin-21 mvn -q -B compile exec:java ``` Observed output: ``` =============== SINGLE CONNECTION =============== CONTROL frame WITH trailing NUL buffers never freed : 0 bytes never freed : 0 ATTACK frame WITHOUT trailing NUL buffers never freed : 1 bytes never freed : 8,132 =============== SUSTAINED (500 CONNECTIONS) =============== connections : 500 buffers never freed : 500 bytes never freed : 4,066,000 (3.9 MiB) leaked per connection: 8,132 bytes ``` The control is the load-bearing part. It sends the byte-for-byte identical frame with its trailing NUL present, and leaks nothing. Because the only difference between the two runs is one byte, the leak is attributable to the missing terminator rather than to the harness or to how `EmbeddedChannel` is torn down. The fix was verified the same way. Recompiling the decoder with the suggested patch and letting it shadow the released class, then re-running the identical PoC, gives 0 buffers and 0 bytes across all 500 connections. ## Impact A remote peer can permanently consume allocator memory in any application using netty's STOMP codec, at one buffer per connection, and can then disconnect. This is reachable before any application-level authentication, since a STOMP frame is sent immediately on connect. Two properties matter more than raw volume here. The memory is never reclaimed. This is not per-connection state that is released when the peer goes away, which is the usual shape of a resource-consumption issue; the buffer outlives the channel entirely. With `PooledByteBufAllocator`, netty's default, the underlying region is never returned to the pool, so neither disconnection nor garbage collection recovers it. Over a long-running process the leak is monotonic. The attacker does not need to hold anything open. Because the leak survives the connection, an attacker can open, leak, and close in a tight loop, presenting a traffic pattern indistinguishable from clients that time out or crash mid-frame. There is no long-lived connection to spot and no rate-limit signature beyond ordinary connection churn. We should be straightforward about what this is not. The leak is roughly one-to-one with bandwidth: leaking an 8,132 byte buffer requires sending 8,132 bytes, because the buffer is only parked once the declared `content-length` has actually been received. There is no amplification. The severity argument rests on permanence, not on ratio - normal traffic accumulates nothing, whereas this accumulates everything and never gives it back. The body size is chosen by th [truncated] Affected: - maven:io.netty:netty-codec-stomp affected <= 4.1.137.Final; fixed unknown - maven:io.netty:netty-codec-stomp affected >= 4.2.0.Final, <= 4.2.17.Final; fixed unknown Fixed versions: see advisory Advisory: https://github.com/netty/netty/security/advisories/GHSA-ghg5-c4jg-8q5j