VYPR
High severityNVD Advisory· Published Oct 19, 2021· Updated Aug 4, 2024

CVE-2021-37137

CVE-2021-37137

Description

The Snappy frame decoder function doesn't restrict the chunk length which may lead to excessive memory usage. Beside this it also may buffer reserved skippable chunks until the whole chunk was received which may lead to excessive memory usage as well. This vulnerability can be triggered by supplying malicious input that decompresses to a very big size (via a network stream or a file) or by sending a huge skippable chunk.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

The Netty Snappy frame decoder does not limit decompressed chunk sizes, allowing a remote attacker to cause excessive memory usage via crafted input.

Vulnerability

The Snappy frame decoder in Netty versions prior to the fix (commit 6da4956 on the 4.1 branch) does not restrict the decompressed chunk length nor limit the accumulation of reserved skippable chunks [1]. This allows an attacker to supply malicious input that decompresses to a very large size (up to 16 MB per compressed chunk, as per the Snappy framing format [2]) or to send a huge skippable chunk that is buffered until fully received [3][4]. The affected code path is in SnappyFrameDecoder.decode() [3][4].

Exploitation

An attacker can trigger the vulnerability by sending a specially crafted network stream or file to a Netty-based application that uses the Snappy frame decoder. No special authentication is required; the input is processed during normal decode operations. The decoder will allocate memory proportional to the claimed decompressed size or buffer the entire skippable chunk, leading to memory exhaustion [1].

Impact

Successful exploitation results in excessive memory consumption, potentially causing an OutOfMemoryError and denial of service. The vulnerability impacts the availability of the application, with no direct impact on confidentiality or integrity [1].

Mitigation

The fix was committed in Netty commit 6da4956 [2]. The patch introduces constants MAX_UNCOMPRESSED_DATA_SIZE (65536 + 4 bytes), MAX_DECOMPRESSED_DATA_SIZE (65536 bytes), and MAX_COMPRESSED_CHUNK_SIZE (16777215 bytes) to restrict chunk lengths, and adds a counter (numBytesToSkip) to avoid buffering the entire skippable chunk [2][3][4]. Users should update to a version of Netty containing this commit (e.g., 4.1.x releases after October 2021).

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
io.netty:netty-codecMaven
>= 4.0.0, < 4.1.68.Final4.1.68.Final
org.jboss.netty:nettyMaven
>= 0
io.netty:nettyMaven
>= 0

Affected products

12

Patches

1
6da4956b3102

Merge pull request from GHSA-9vjp-v76f-g363

https://github.com/netty/nettyNorman MaurerSep 9, 2021via ghsa
2 files changed · +62 14
  • codec/src/main/java/io/netty/handler/codec/compression/SnappyFrameDecoder.java+39 7 modified
    @@ -45,13 +45,19 @@ private enum ChunkType {
         }
     
         private static final int SNAPPY_IDENTIFIER_LEN = 6;
    +    // See https://github.com/google/snappy/blob/1.1.9/framing_format.txt#L95
         private static final int MAX_UNCOMPRESSED_DATA_SIZE = 65536 + 4;
    +    // See https://github.com/google/snappy/blob/1.1.9/framing_format.txt#L82
    +    private static final int MAX_DECOMPRESSED_DATA_SIZE = 65536;
    +    // See https://github.com/google/snappy/blob/1.1.9/framing_format.txt#L82
    +    private static final int MAX_COMPRESSED_CHUNK_SIZE = 16777216 - 1;
     
         private final Snappy snappy = new Snappy();
         private final boolean validateChecksums;
     
         private boolean started;
         private boolean corrupted;
    +    private int numBytesToSkip;
     
         /**
          * Creates a new snappy-framed decoder with validation of checksums
    @@ -82,6 +88,16 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) t
                 return;
             }
     
    +        if (numBytesToSkip != 0) {
    +            // The last chunkType we detected was RESERVED_SKIPPABLE and we still have some bytes to skip.
    +            int skipBytes = Math.min(numBytesToSkip, in.readableBytes());
    +            in.skipBytes(skipBytes);
    +            numBytesToSkip -= skipBytes;
    +
    +            // Let's return and try again.
    +            return;
    +        }
    +
             try {
                 int idx = in.readerIndex();
                 final int inSize = in.readableBytes();
    @@ -123,12 +139,15 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) t
                             throw new DecompressionException("Received RESERVED_SKIPPABLE tag before STREAM_IDENTIFIER");
                         }
     
    -                    if (inSize < 4 + chunkLength) {
    -                        // TODO: Don't keep skippable bytes
    -                        return;
    -                    }
    +                    in.skipBytes(4);
     
    -                    in.skipBytes(4 + chunkLength);
    +                    int skipBytes = Math.min(chunkLength, in.readableBytes());
    +                    in.skipBytes(skipBytes);
    +                    if (skipBytes != chunkLength) {
    +                        // We could skip all bytes, let's store the remaining so we can do so once we receive more
    +                        // data.
    +                        numBytesToSkip = chunkLength - skipBytes;
    +                    }
                         break;
                     case RESERVED_UNSKIPPABLE:
                         // The spec mandates that reserved unskippable chunks must immediately
    @@ -141,7 +160,8 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) t
                             throw new DecompressionException("Received UNCOMPRESSED_DATA tag before STREAM_IDENTIFIER");
                         }
                         if (chunkLength > MAX_UNCOMPRESSED_DATA_SIZE) {
    -                        throw new DecompressionException("Received UNCOMPRESSED_DATA larger than 65540 bytes");
    +                        throw new DecompressionException("Received UNCOMPRESSED_DATA larger than " +
    +                                MAX_UNCOMPRESSED_DATA_SIZE + " bytes");
                         }
     
                         if (inSize < 4 + chunkLength) {
    @@ -162,13 +182,25 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) t
                             throw new DecompressionException("Received COMPRESSED_DATA tag before STREAM_IDENTIFIER");
                         }
     
    +                    if (chunkLength > MAX_COMPRESSED_CHUNK_SIZE) {
    +                        throw new DecompressionException("Received COMPRESSED_DATA that contains" +
    +                                " chunk that exceeds " + MAX_COMPRESSED_CHUNK_SIZE + " bytes");
    +                    }
    +
                         if (inSize < 4 + chunkLength) {
                             return;
                         }
     
                         in.skipBytes(4);
                         int checksum = in.readIntLE();
    -                    ByteBuf uncompressed = ctx.alloc().buffer();
    +
    +                    int uncompressedSize = snappy.getPreamble(in);
    +                    if (uncompressedSize > MAX_DECOMPRESSED_DATA_SIZE) {
    +                        throw new DecompressionException("Received COMPRESSED_DATA that contains" +
    +                                " uncompressed data that exceeds " + MAX_DECOMPRESSED_DATA_SIZE + " bytes");
    +                    }
    +
    +                    ByteBuf uncompressed = ctx.alloc().buffer(uncompressedSize, MAX_DECOMPRESSED_DATA_SIZE);
                         try {
                             if (validateChecksums) {
                                 int oldWriterIndex = in.writerIndex();
    
  • codec/src/main/java/io/netty/handler/codec/compression/Snappy.java+23 7 modified
    @@ -38,20 +38,19 @@ public final class Snappy {
         private static final int COPY_2_BYTE_OFFSET = 2;
         private static final int COPY_4_BYTE_OFFSET = 3;
     
    -    private State state = State.READY;
    +    private State state = State.READING_PREAMBLE;
         private byte tag;
         private int written;
     
         private enum State {
    -        READY,
             READING_PREAMBLE,
             READING_TAG,
             READING_LITERAL,
             READING_COPY
         }
     
         public void reset() {
    -        state = State.READY;
    +        state = State.READING_PREAMBLE;
             tag = 0;
             written = 0;
         }
    @@ -270,9 +269,6 @@ private static void encodeCopy(ByteBuf out, int offset, int length) {
         public void decode(ByteBuf in, ByteBuf out) {
             while (in.isReadable()) {
                 switch (state) {
    -            case READY:
    -                state = State.READING_PREAMBLE;
    -                // fall through
                 case READING_PREAMBLE:
                     int uncompressedLength = readPreamble(in);
                     if (uncompressedLength == PREAMBLE_NOT_FULL) {
    @@ -281,7 +277,6 @@ public void decode(ByteBuf in, ByteBuf out) {
                     }
                     if (uncompressedLength == 0) {
                         // Should never happen, but it does mean we have nothing further to do
    -                    state = State.READY;
                         return;
                     }
                     out.ensureWritable(uncompressedLength);
    @@ -378,6 +373,27 @@ private static int readPreamble(ByteBuf in) {
             return 0;
         }
     
    +    /**
    +     * Get the length varint (a series of bytes, where the lower 7 bits
    +     * are data and the upper bit is a flag to indicate more bytes to be
    +     * read).
    +     *
    +     * @param in The input buffer to get the preamble from
    +     * @return The calculated length based on the input buffer, or 0 if
    +     *   no preamble is able to be calculated
    +     */
    +    int getPreamble(ByteBuf in) {
    +        if (state == State.READING_PREAMBLE) {
    +            int readerIndex = in.readerIndex();
    +            try {
    +                return readPreamble(in);
    +            } finally {
    +                in.readerIndex(readerIndex);
    +            }
    +        }
    +        return 0;
    +    }
    +
         /**
          * Reads a literal from the input buffer directly to the output buffer.
          * A "literal" is an uncompressed segment of data stored directly in the
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

26

News mentions

0

No linked articles in our index yet.