CVE-2024-36114: Insufficient Information (NVD-CWE-noinfo)
value: Aircompressor is a library with ports of the Snappy, LZO, LZ4, and Zstandard compression algorithms to Java. All decompressor implementations of Aircompressor (LZ4, LZO, Snappy, Zstandard) can crash the JVM for certain input, and in some cases also leak the content of other memory of the Java process (which could contain sensitive information). When decompressing certain data, the decompressors try to access memory outside the bounds of the given byte arrays or byte buffers. Because Aircompressor uses the JDK class `sun.misc.Unsafe` to speed up memory access, no additional bounds checks are performed and this has similar security consequences as out-of-bounds access in C or C++, namely it can lead to non-deterministic behavior or crash the JVM. Users should update to Aircompressor 0.27 or newer where these issues have been fixed. When decompressing data from untrusted users, this can be exploited for a denial-of-service attack by crashing the JVM, or to leak other sensitive information from the Java process. There are no known workarounds for this issue.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-29T21:15Z
Repository: https://github.com/airlift/aircompressor
Language: Java
Files changed: src/main/java/io/airlift/compress/lz4/Lz4RawDecompressor.java, src/main/java/io/airlift/compress/lzo/LzoRawDecompressor.java, src/main/java/io/airlift/compress/snappy/SnappyRawDecompressor.java, src/main/java/io/airlift/compress/zstd/Huffman.java, src/test/java/io/airlift/compress/lz4/TestLz4.java, src/test/java/io/airlift/compress/lzo/TestLzo.java, src/test/java/io/airlift/compress/snappy/TestSnappy.java, src/test/java/io/airlift/compress/zstd/TestZstd.java
Commit date: 2024-02-13 08:10:53 -0800
Commit message: Fix out of bounds read due to negative length

If the computed literal or match length was greater than
Integer.MAX_VALUE, the value would overflow to a negative
and cause a read before the beginning of the buffer.

Diff: diff --git a/src/main/java/io/airlift/compress/lz4/Lz4RawDecompressor.java b/src/main/java/io/airlift/compress/lz4/Lz4RawDecompressor.java
index 60183d7..7993ad3 100644
--- a/src/main/java/io/airlift/compress/lz4/Lz4RawDecompressor.java
+++ b/src/main/java/io/airlift/compress/lz4/Lz4RawDecompressor.java
@@ -67,10 +67,13 @@ public final class Lz4RawDecompressor
                     value = UNSAFE.getByte(inputBase, input++) & 0xFF;
                     literalLength += value;
                 }
                 while (value == 255 && input < inputLimit - 15);
             }
+            if (literalLength < 0) {
+                throw new MalformedInputException(input - inputAddress);
+            }
 
             // copy literal
             long literalEnd = input + literalLength;
             long literalOutputLimit = output + literalLength;
             if (literalOutputLimit > (fastOutputLimit - MIN_MATCH) || literalEnd > inputLimit - (OFFSET_SIZE + TOKEN_SIZE + LAST_LITERAL_SIZE)) {
@@ -125,10 +128,13 @@ public final class Lz4RawDecompressor
                     matchLength += value;
                 }
                 while (value == 255);
             }
             matchLength += MIN_MATCH; // implicit length from initial 4-byte match in encoder
+            if (matchLength < 0) {
+                throw new MalformedInputException(input - inputAddress);
+            }
 
             long matchOutputLimit = output + matchLength;
 
             // at this point we have at least 12 bytes of space in the output buffer
             // due to the fastLimit check before copying a literal, so no need to check again
diff --git a/src/main/java/io/airlift/compress/lzo/LzoRawDecompressor.java b/src/main/java/io/airlift/compress/lzo/LzoRawDecompressor.java
index bb30c2d..2a8dcc4 100644
--- a/src/main/java/io/airlift/compress/lzo/LzoRawDecompressor.java
+++ b/src/main/java/io/airlift/compress/lzo/LzoRawDecompressor.java
@@ -246,10 +246,14 @@ public final class LzoRawDecompressor
                     String binary = toBinary(command);
                     throw new MalformedInputException(input - 1, "Invalid LZO command " + binary);
                 }
                 firstCommand = false;
 
+                if (matchLength < 0) {
+                    throw new MalformedInputException(input - inputAddress);
+                }
+
                 // copy match
                 if (matchLength != 0) {
                     // lzo encodes match offset minus one
                     matchOffset++;
 
@@ -314,10 +318,13 @@ public final class LzoRawDecompressor
                     }
                     output = matchOutputLimit; // correction in case we over-copied
                 }
 
                 // copy literal
+                if (literalLength < 0) {
+                    throw new MalformedInputException(input - inputAddress);
+                }
                 long literalOutputLimit = output + literalLength;
                 if (literalOutputLimit > fastOutputLimit || input + literalLength > inputLimit - SIZE_OF_LONG) {
                     if (literalOutputLimit > outputLimit) {
                         throw new MalformedInputException(input - inputAddress);
                     }
diff --git a/src/main/java/io/airlift/compress/snappy/SnappyRawDecompressor.java b/src/main/java/io/airlift/compress/snappy/SnappyRawDecompressor.java
index 7cca9af..2871b9a 100644
--- a/src/main/java/io/airlift/compress/snappy/SnappyRawDecompressor.java
+++ b/src/main/java/io/airlift/compress/snappy/SnappyRawDecompressor.java
@@ -114,10 +114,13 @@ public final class SnappyRawDecompressor
                 continue;
             }
 
             if ((opCode & 0x3) == LITERAL) {
                 int literalLength = length + trailer;
+                if (literalLength < 0) {
+                    throw new MalformedInputException(input - inputAddress);
+                }
 
                 // copy literal
                 long literalOutputLimit = output + literalLength;
                 if (literalOutputLimit > fastOutputLimit || input + literalLength > inputLimit - SIZE_OF_LONG) {
                     if (literalOutputLimit > outputLimit || input + literalLength > inputLimit) {
@@ -145,10 +148,13 @@ public final class SnappyRawDecompressor
                 // matchOffset/256 is encoded in bits 8..10.  By just fetching
                 // those bits, we get matchOffset (since the bit-field starts at
                 // bit 8).
                 int matchOffset = entry & 0x700;
                 matchOffset += trailer;
+                if (matchOffset < 0) {
+                    throw new MalformedInputException(input - inputAddress);
+                }
 
                 long matchAddress = output - matchOffset;
                 if (matchAddress < outputAddress || output + length > outputLimit) {
                     throw new MalformedInputException(input - inputAddress);
                 }
diff --git a/src/main/java/io/airlift/compress/zstd/Huffman.java b/src/main/java/io/airlift/compress/zstd/Huffman.java
index b5e24e2..29fb74e 100644
--- a/src/main/java/io/airlift/compress/zstd/Huffman.java
+++ b/src/main/java/io/airlift/compress/zstd/Huffman.java
@@ -170,10 +170,12 @@ class Huffman
         long start1 = inputAddress + 3 * SIZE_OF_SHORT; // for the shorts we read below
         long start2 = start1 + (UNSAFE.getShort(inputBase, inputAddress) & 0xFFFF);
         long start3 = start2 + (UNSAFE.getShort(inputBase, inputAddress + 2) & 0xFFFF);
         long start4 = start3 + (UNSAFE.getShort(inputBase, inputAddress + 4) & 0xFFFF);
 
+        verify(start2 < start3 && start3 < start4 && start4 < inputLimit, inputAddress, "Input is corrupted");
+
         BitInputStream.Initializer initializer = new BitInputStream.Initializer(inputBase, start1, start2);
         initializer.initialize();
         int stream1bitsConsumed = initializer.getBitsConsumed();
         long stream1currentAddress = initializer.getCurrentAddress();
         long stream1bits = initializer.getBits();
diff --git a/src/test/java/io/airlift/compress/lz4/TestLz4.java b/src/test/java/io/airlift/compress/lz4/TestLz4.java
index 3f9a13e..03a6964 100644
--- a/src/test/java/io/airlift/compress/lz4/TestLz4.java
+++ b/src/test/java/io/airlift/compress/lz4/TestLz4.java
@@ -14,13 +14,21 @@
 package io.airlift.compress.lz4;
 
 import io.airlift.compress.AbstractTestCompression;
 import io.airlift.compress.Compressor;
 import io.airlift.compress.Decompressor;
+import io.airlift.compress.MalformedInputException;
 import io.airlift.compress.thirdparty.JPountzLz4Compressor;
 import io.airlift.compress.thirdparty.JPountzLz4Decompressor;
 import net.jpountz.lz4.LZ4Factory;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 public class TestLz4
         extends AbstractTestCompression
 {
     @Override
@@ -44,6 +52,47 @@ public class TestLz4
     @Override
     protected Decompressor getVerifyDecompressor()
     {
         return new JPountzLz4Decompressor(LZ4Factory.fastestInstance());
     }
+
+    @Test
+    public void testLiteralLengthOverflow()
+            throws IOException
+    {
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        buffer.write((byte) 0b1111_0000); // token
+        // Causes overflow for `literalLength`
+        byte[] literalLengthBytes = new byte[Integer.MAX_VALUE / 255 + 1]; // ~9MB
+        Arrays.fill(literalLengthBytes, (byte) 255);
+        buffer.write(literalLengthBytes);
+        buffer.write(1);
+        buffer.write(new byte[20]);
+
+        byte[] data = buffer.toByteArray();
+
+        assertThatThrownBy(() -> new Lz4Decompressor().decompress(data, 0, data.length, new byte[2048], 0, 2048))
+                .isInstanceOf(MalformedInputException.class);
+    }
+
+    @Test
+    public void testMatchLengthOverflow()
+            throws IOException
+    {
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        buffer.write((byte) 0b0000_1111); // token
+        buffer.write(new byte[2]); // offset
+
+        // Causes overflow for `matchLength`
+        byte[] literalLengthBytes = new byte[Integer.MAX_VALUE / 255 + 1]; // ~9MB
+        Arrays.fill(literalLengthBytes, (byte) 255);
+        buffer.write(literalLengthBytes);
+        buffer.write(1);
+
+        buffer.write(new byte[10]);
+
+        byte[] data = buffer.toByteArray();
+
+        assertThatThrownBy(() -> new Lz4Decompressor().decompress(data, 0, data.length, new byte[2048], 0, 2048))
+                .isInstanceOf(MalformedInputException.class);
+    }
 }
diff --git a/src/test/java/io/airlift/compress/lzo/TestLzo.java b/src/test/java/io/airlift/compress/lzo/TestLzo.java
index b7dfe6f..9aaa1a2 100644
--- a/src/test/java/io/airlift/compress/lzo/TestLzo.java
+++ b/src/test/java/io/airlift/compress/lzo/TestLzo.java
@@ -15,12 +15,19 @@ package io.airlift.compress.lzo;
 
 import io.airlift.compress.AbstractTestCompression;
 import io.airlift.compress.Compressor;
 import io.airlift.compress.Decompressor;
 import io.airlift.compress.HadoopNative;
+import io.airlift.compress.MalformedInputException;
 import io.airlift.compress.thirdparty.HadoopLzoCompressor;
 import io.airlift.compress.thirdparty.HadoopLzoDecompressor;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 public class TestLzo
         extends AbstractTestCompression
 {
     static {
@@ -48,6 +55,80 @@ public class TestLzo
     @Override
     protected Decompressor getVerifyDecompressor()
     {
         return new HadoopLzoDecompressor();
     }
+
+    @Test
+    public void testLiteralLengthOverflow()
+            throws IOException
+    {
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        // Command
+        buffer.write(0);
+        // Causes overflow for `literalLength`
+        buffer.write(new byte[Integer.MAX_VALUE / 255 + 1]); // ~9MB
+        buffer.write(1);
+
+        byte[] data = buffer.toByteArray();
+
+        assertThatThrownBy(() -> new LzoDecompressor().decompress(data, 0, data.length, new byte[20000], 0, 20000))
+                .isInstanceOf(MalformedInputException.class);
+    }
+
+    @Test
+    public void testMatchLengthOverflow1()
+            throws IOException
+    {
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        // Write some data so that `matchOffset` validation later passes
+        // Command
+        buffer.write(0);
+        buffer.write(new byte[66]);
+        buffer.write(8);
+        buffer.write(new byte[2107 * 8]);
+
+        // Command
+        buffer.write(0b001_0000);
+        // Causes overflow for `matchLength`
+        buffer.write(new byte[Integer.MAX_VALUE / 255 + 1]); // ~9MB
+        buffer.write(1);
+        // Trailer
+        buffer.write(0b0000_0000);
+        buffer.write(0b0000_0100);
+
+        buffer.write(new byte[10]);
+
+        byte[] data = buffer.toByteArray();
+
+        assertThatThrownBy(() -> new LzoDecompressor().decompress(data, 0, data.length, new byte[20000], 0, 20000))
+                .isInstanceOf(MalformedInputException.class);
+    }
+
+    @Test
+    public void testMatchLengthOverflow2()
+            throws IOException
+    {
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        // Write some data so that `matchOffset` validation later passes
+        // Command
+        buffer.write(0);
+        buffer.write(246);
+        buffer.write(new byte[264]);
+
+        // Command
+        buffer.write(0b0010_0000);
+        // Causes overflow for `matchLength`
+        buffer.write(new byte[Integer.MAX_VALUE / 255 + 1]); // ~9MB
+        buffer.write(1);
+        // Trailer
+        buffer.write(0b0000_0000);
+        buffer.write(0b0000_0100);
+
+        buffer.write(new byte[10]);
+
+        byte[] data = buffer.toByteArray();
+
+        assertThatThrownBy(() -> new LzoDecompressor().decompress(data, 0, data.length, new byte[20000], 0, 20000))
+                .isInstanceOf(MalformedInputException.class);
+    }
 }
diff --git a/src/test/java/io/airlift/compress/snappy/TestSnappy.java b/src/test/java/io/airlift/compress/snappy/TestSnappy.java
index 91c14cb..b2e92d7 100644
--- a/src/test/java/io/airlift/compress/snappy/TestSnappy.java
+++ b/src/test/java/io/airlift/compress/snappy/TestSnappy.java
@@ -14,12 +14,16 @@
 package io.airlift.compress.snappy;
 
 import io.airlift.compress.AbstractTestCompression;
 import io.airlift.compress.Compressor;
 import io.airlift.compress.Decompressor;
+import io.airlift.compress.MalformedInputException;
 import io.airlift.compress.thirdparty.XerialSnappyCompressor;
 import io.airlift.compress.thirdparty.XerialSnappyDecompressor;
+import org.testng.annotations.Test;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 public class TestSnappy
         extends AbstractTestCompression
 {
     @Override
@@ -43,6 +47,24 @@ public class TestSnappy
     @Override
     protected Decompressor getVerifyDecompressor()
     {
         return new XerialSnappyDecompressor();
     }
+
+    @Test
+    public void testInvalidLiteralLength()
+    {
+        byte[] data = {
+                // Encoded uncompressed length 1024
+                -128, 8,
+                // op-code
+                (byte) 252,
+                // Trailer value Integer.MAX_VALUE
+                (byte) 0b1111_1111, (byte) 0b1111_1111, (byte) 0b1111_1111, (byte) 0b0111_1111,
+                // Some arbitrary data
+                0, 0, 0, 0, 0, 0, 0, 0
+        };
+
+        assertThatThrownBy(() -> new SnappyDecompressor().decompress(data, 0, data.length, new byte[1024], 0, 1024))
+                .isInstanceOf(MalformedInputException.class);
+    }
 }
diff --git a/src/test/java/io/airlift/compress/zstd/TestZstd.java b/src/test/java/io/airlift/compress/zstd/TestZstd.java
index 8c93b46..ba3dd22 100644
--- a/src/test/java/io/airlift/compress/zstd/TestZstd.java
+++ b/src/test/java/io/airlift/compress/zstd/TestZstd.java
@@ -21,10 +21,11 @@ import io.airlift.compress.MalformedInputException;
 import io.airlift.compress.benchmark.DataSet;
 import io.airlift.compress.thirdparty.ZstdJniCompressor;
 import io.airlift.compress.thirdparty.ZstdJniDecompressor;
 import org.testng.annotations.Test;
 
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.util.Arrays;
 
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -207,6 +208,56 @@ public class TestZstd
         byte[] output = new byte[1024];
         assertThatThrownBy(() -> getDecompressor().decompress(input, 0, input.length, output, 0, output.length))
                 .matches(e -> e instanceof MalformedInputException || e instanceof UncheckedIOException)
                 .hasMessageContaining("Not enough input bytes");
     }
+
+    @Test
+    public void testBadHuffmanData()
+            throws IOException
+    {
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        // Magic
+        buffer.write(new byte[] {
+                (byte) 0b0010_1000,
+                (byte) 0b1011_0101,
+                (byte) 0b0010_1111,
+                (byte) 0b1111_1101,
+        });
+        // Frame header
+        buffer.write(0);
+        buffer.write(0);
+        // Block header COMPRESSED_BLOCK
+        buffer.write(new byte[] {
+                (byte) 0b1111_0100,
+                (byte) 0b0000_0000,
+                (byte) 0b0000_0000,
+        });
+        // Literals header
+        buffer.write(new byte[] {
+                // literalsBlockType COMPRESSED_LITERALS_BLOCK
+                // + literals type
+                0b0000_1010,
+                // ... header remainder
+                0b0000_0000,
+                // compressedSize
+                0b0011_1100,
+                0b0000_0000,
+        });
+        // Huffman inputSize
+        buffer.write(128);
+        // weight value
+        buffer.write(0b0001_0000);
+        // Bad start values
+        buffer.write(new byte[] {(byte) 255, (byte) 255});
+        buffer.write(new byte[] {(byte) 255, (byte) 255});
+        buffer.write(new byte[] {(byte) 255, (byte) 255});
+
+        buffer.write(new byte[10]);
+
+        byte[] data = buffer.toByteArray();
+
+        assertThatThrownBy(() -> new ZstdDecompressor().decompress(data, 0, data.length, new byte[10], 0, 10))
+                .isInstanceOf(MalformedInputException.class)
+                .hasMessageStartingWith("Input is corrupted");
+    }
 }

Fixed code:             if (literalLength < 0) {
                throw new MalformedInputException(input - inputAddress);
            }
            if (matchLength < 0) {
                throw new MalformedInputException(input - inputAddress);
            }
                if (matchLength < 0) {
                    throw new MalformedInputException(input - inputAddress);
                }

                if (literalLength < 0) {
                    throw new MalformedInputException(input - inputAddress);
                }
                if (literalLength < 0) {
                    throw new MalformedInputException(input - inputAddress);
                }
                if (matchOffset < 0) {
                    throw new MalformedInputException(input - inputAddress);
                }
        verify(start2 < start3 && start3 < start4 && start4 < inputLimit, inputAddress, "Input is corrupted");

import io.airlift.compress.MalformedInputException;
import org.testng.annotations.Test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

    @Test
    public void testLiteralLengthOverflow()
            throws IOException
    {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        buffer.write((byte) 0b1111_0000); // token
        // Causes overflow for `literalLength`
        byte[] literalLengthBytes = new byte[Integer.MAX_VALUE / 255 + 1]; // ~9MB
        Arrays.fill(literalLengthBytes, (byte) 255);
        buffer.write(literalLengthBytes);
        buffer.write(1);
        buffer.write(new byte[20]);

        byte[] data = buffer.toByteArray();

        assertThatThrownBy(() -> new Lz4Decompressor().decompress(data, 0, data.length, new byte[2048], 0, 2048))
                .isInstanceOf(MalformedInputException.class);
    }

    @Test
    public void testMatchLengthOverflow()
            throws IOException
    {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        buffer.write((byte) 0b0000_1111); // token
        buffer.write(new byte[2]); // offset

        // Causes overflow for `matchLength`
        byte[] literalLengthBytes = new byte[Integer.MAX_VALUE / 255 + 1]; // ~9MB
        Arrays.fill(literalLengthBytes, (byte) 255);
        buffer.write(literalLengthBytes);
        buffer.write(1);

        buffer.write(new byte[10]);

        byte[] data = buffer.toByteArray();

        assertThatThrownBy(() -> new Lz4Decompressor().decompress(data, 0, data.length, new byte[2048], 0, 2048))
                .isInstanceOf(MalformedInputException.class);
    }
import io.airlift.compress.MalformedInputException;
import org.testng.annotations.Test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

    @Test
    public void testLiteralLengthOverflow()
            throws IOException
    {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        // Command
        buffer.write(0);
        // Causes overflow for `literalLength`
        buffer.write(new byte[Integer.MAX_VALUE / 255 + 1]); // ~9MB
        buffer.write(1);

        byte[] data = buffer.toByteArray();

        assertThatThrownBy(() -> new LzoDecompressor().decompress(data, 0, data.length, new byte[20000], 0, 20000))
                .isInstanceOf(MalformedInputException.class);
    }

    @Test
    public void testMatchLengthOverflow1()
            throws IOException
    {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        // Write some data so that `matchOffset` validation later passes
        // Command
        buffer.write(0);
        buffer.write(new byte[66]);
        buffer.write(8);
        buffer.write(new byte[2107 * 8]);

        // Command
        buffer.write(0b001_0000);
        // Causes overflow for `matchLength`
        buffer.write(new byte[Integer.MAX_VALUE / 255 + 1]); // ~9MB
        buffer.write(1);
        // Trailer
        buffer.write(0b0000_0000);
        buffer.write(0b0000_0100);

        buffer.write(new byte[10]);

        byte[] data = buffer.toByteArray();

        assertThatThrownBy(() -> new LzoDecompressor().decompress(data, 0, data.length, new byte[20000], 0, 20000))
                .isInstanceOf(MalformedInputException.class);
    }

    @Test
    public void testMatchLengthOverflow2()
            throws IOException
    {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        // Write some data so that `matchOffset` validation later passes
        // Command
        buffer.write(0);
        buffer.write(246);
        buffer.write(new byte[264]);

        // Command
        buffer.write(0b0010_0000);
        // Causes overflow for `matchLength`
        buffer.write(new byte[Integer.MAX_VALUE / 255 + 1]); // ~9MB
        buffer.write(1);
        // Trailer
        buffer.write(0b0000_0000);
        buffer.write(0b0000_0100);

        buffer.write(new byte[10]);

        byte[] data = buffer.toByteArray();

        assertThatThrownBy(() -> new LzoDecompressor().decompress(data, 0, data.length, new byte[20000], 0, 20000))
                .isInstanceOf(MalformedInputException.class);
    }
import io.airlift.compress.MalformedInputException;
import org.testng.annotations.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

    @Test
    public void testInvalidLiteralLength()
    {
        byte[] data = {
                // Encoded uncompressed length 1024
                -128, 8,
                // op-code
                (byte) 252,
                // Trailer value Integer.MAX_VALUE
                (byte) 0b1111_1111, (byte) 0b1111_1111, (byte) 0b1111_1111, (byte) 0b0111_1111,
                // Some arbitrary data
                0, 0, 0, 0, 0, 0, 0, 0
        };

        assertThatThrownBy(() -> new SnappyDecompressor().decompress(data, 0, data.length, new byte[1024], 0, 1024))
                .isInstanceOf(MalformedInputException.class);
    }
import java.io.ByteArrayOutputStream;

    @Test
    public void testBadHuffmanData()
            throws IOException
    {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        // Magic
        buffer.write(new byte[] {
                (byte) 0b0010_1000,
                (byte) 0b1011_0101,
                (byte) 0b0010_1111,
                (byte) 0b1111_1101,
        });
        // Frame header
        buffer.write(0);
        buffer.write(0);
        // Block header COMPRESSED_BLOCK
        buffer.write(new byte[] {
                (byte) 0b1111_0100,
                (byte) 0b0000_0000,
                (byte) 0b0000_0000,
        });
        // Literals header
        buffer.write(new byte[] {
                // literalsBlockType COMPRESSED_LITERALS_BLOCK
                // + literals type
                0b0000_1010,
                // ... header remainder
                0b0000_0000,
                // compressedSize
                0b0011_1100,
                0b0000_0000,
        });
        // Huffman inputSize
        buffer.write(128);
        // weight value
        buffer.write(0b0001_0000);
        // Bad start values
        buffer.write(new byte[] {(byte) 255, (byte) 255});
        buffer.write(new byte[] {(byte) 255, (byte) 255});
        buffer.write(new byte[] {(byte) 255, (byte) 255});

        buffer.write(new byte[10]);

        byte[] data = buffer.toByteArray();

        assertThatThrownBy(() -> new ZstdDecompressor().decompress(data, 0, data.length, new byte[10], 0, 10))
                .isInstanceOf(MalformedInputException.class)
                .hasMessageStartingWith("Input is corrupted");
    }