You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2026/08/17 11:12:02 UTC

(commons-compress) 02/02: Avoid unbounded recursion on empty frames in framed lz4 and snappy decoders (#803).

This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-compress.git

commit bfc5bb0932baed62c71ea9fabe6e46383dceda46
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Mon Aug 17 07:11:54 2026 -0400

    Avoid unbounded recursion on empty frames in framed lz4 and snappy
    decoders (#803).
    
    - Add comments.
    - Sort members.
---
 src/changes/changes.xml                            |  2 ++
 .../lz4/FramedLZ4CompressorInputStream.java        |  1 +
 .../snappy/FramedSnappyCompressorInputStream.java  |  1 +
 .../lz4/FramedLZ4CompressorInputStreamTest.java    | 38 +++++++++++-----------
 .../FramedSnappyCompressorInputStreamTest.java     | 34 +++++++++----------
 5 files changed, 40 insertions(+), 36 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index f4e45f82a..944117d21 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -152,6 +152,7 @@ The <action> type attribute can be add,update,fix,remove.
       <action type="fix" dev="ggregory" due-to="Gary Gregory, KALI 834X">[GZip] GzipCompressorInputStream now throws CompressorException instead of IllegalArgumetException/IllegalStateException.</action>
       <!-- FIX snappy -->
       <action type="fix" dev="ggregory" due-to="Stanislav Fort, Gary Gregory">[Snappy] Fix for when a valid raw Snappy stream with uncompressed size > 2 GiB used to decompress and then fail at physical EOF with a “Premature end of stream” exception instead of completing cleanly.</action>
+      <action type="fix" dev="ggregory" due-to="KALI 834X, Gary Gregory">[Snappy] Avoid unbounded recursion on empty frames in framed snappy decoders (#803).</action>
       <!-- FIX deflate64 -->
       <action type="fix" dev="ggregory" due-to="KALI 834X, Gary Gregory">[Deflate64] Reject invalid literal/length and distance codes in Deflate64 decoder (#785).</action>
       <action type="fix" dev="ggregory" due-to="Gary Gregory">[Deflate64] Deflate64CompressorInputStream now throws CompressorException instead of IllegalArgumetException/IllegalStateException.</action>
@@ -162,6 +163,7 @@ The <action> type attribute can be add,update,fix,remove.
       <action type="fix" dev="ggregory" due-to="Gary Gregory">[LZ77] LZ77Compressor.prefill(byte[]) now throws CompressorException instead of IllegalArgumetException/IllegalStateException.</action>
       <!-- FIX lz4 -->
       <action type="fix" dev="ggregory" due-to="Gary Gregory">[LZ4] BlockLZ4CompressorOutputStream now throws CompressorException instead of IllegalArgumetException/IllegalStateException.</action>
+      <action type="fix" dev="ggregory" due-to="KALI 834X, Gary Gregory">[LZ4] Avoid unbounded recursion on empty frames in framed lz4 decoders (#803).</action>
       <!-- FIX lzw -->
       <action type="fix" dev="ggregory" due-to="Gary Gregory">[LZW] LZWInputStream now throws CompressorException instead of IllegalArgumetException/IllegalStateException.</action>
       <!-- FIX general -->      
diff --git a/src/main/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStream.java b/src/main/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStream.java
index 11a0f1581..e3f2424d2 100644
--- a/src/main/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStream.java
+++ b/src/main/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStream.java
@@ -194,6 +194,7 @@ private void maybeFinishCurrentBlock() throws IOException {
 
     private void nextBlock() throws IOException {
         while (true) {
+            // loop terminates when a real block is found or EOF is reached.
             maybeFinishCurrentBlock();
             final long len = ByteUtils.fromLittleEndian(supplier, 4);
             final boolean uncompressed = (len & UNCOMPRESSED_FLAG_MASK) != 0;
diff --git a/src/main/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStream.java b/src/main/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStream.java
index b3ab270b5..264672160 100644
--- a/src/main/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStream.java
+++ b/src/main/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStream.java
@@ -227,6 +227,7 @@ private long readCrc() throws IOException {
 
     private void readNextBlock() throws IOException {
         while (true) {
+            // loop terminates when a real block is found or EOF is reached.
             verifyLastChecksumAndReset();
             inUncompressedChunk = false;
             final int type = readOneByte();
diff --git a/src/test/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStreamTest.java b/src/test/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStreamTest.java
index 63ad2c65e..2d10e6af6 100644
--- a/src/test/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStreamTest.java
+++ b/src/test/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStreamTest.java
@@ -91,6 +91,25 @@ void testBackreferenceWithOffsetTooBigCausesIOException() {
         expectIOException("COMPRESS-490/ArrayIndexOutOfBoundsException2.lz4");
     }
 
+    @Test
+    void testManyEmptyConcatenatedFramesDoNotOverflowStack() throws IOException {
+        final byte[] singleEmptyFrame;
+        try (ByteArrayOutputStream frame = new ByteArrayOutputStream()) {
+            new FramedLZ4CompressorOutputStream(frame).close();
+            singleEmptyFrame = frame.toByteArray();
+        }
+        final ByteArrayOutputStream concatenated = new ByteArrayOutputStream();
+        for (int i = 0; i < 200_000; i++) {
+            concatenated.write(singleEmptyFrame);
+        }
+        final byte[] input = concatenated.toByteArray();
+        assertDoesNotThrow(() -> {
+            try (FramedLZ4CompressorInputStream in = new FramedLZ4CompressorInputStream(new ByteArrayInputStream(input), true)) {
+                assertEquals(-1, in.read());
+            }
+        });
+    }
+
     @Test
     void testMatches() throws IOException {
         assertFalse(FramedLZ4CompressorInputStream.matches(new byte[10], 4));
@@ -553,25 +572,6 @@ void testSkipsOverSkippableFrames() throws IOException {
         }
     }
 
-    @Test
-    void testManyEmptyConcatenatedFramesDoNotOverflowStack() throws IOException {
-        final byte[] singleEmptyFrame;
-        try (ByteArrayOutputStream frame = new ByteArrayOutputStream()) {
-            new FramedLZ4CompressorOutputStream(frame).close();
-            singleEmptyFrame = frame.toByteArray();
-        }
-        final ByteArrayOutputStream concatenated = new ByteArrayOutputStream();
-        for (int i = 0; i < 200_000; i++) {
-            concatenated.write(singleEmptyFrame);
-        }
-        final byte[] input = concatenated.toByteArray();
-        assertDoesNotThrow(() -> {
-            try (FramedLZ4CompressorInputStream in = new FramedLZ4CompressorInputStream(new ByteArrayInputStream(input), true)) {
-                assertEquals(-1, in.read());
-            }
-        });
-    }
-
     @Test
     void testSkipsOverTrailingSkippableFrames() throws IOException {
         final byte[] input = { 4, 0x22, 0x4d, 0x18, // signature
diff --git a/src/test/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStreamTest.java b/src/test/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStreamTest.java
index 64edb039a..0cc93e641 100644
--- a/src/test/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStreamTest.java
+++ b/src/test/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStreamTest.java
@@ -109,6 +109,23 @@ void testLoremIpsum() throws Exception {
         assertArrayEquals(Files.readAllBytes(outputSz), Files.readAllBytes(outputGz));
     }
 
+    @Test
+    void testManyPaddingChunksDoNotOverflowStack() {
+        final byte[] streamIdentifier = { (byte) 0xff, 6, 0, 0, 's', 'N', 'a', 'P', 'p', 'Y' };
+        final byte[] emptyPaddingChunk = { (byte) 0xfe, 0, 0, 0 };
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        out.write(streamIdentifier, 0, streamIdentifier.length);
+        for (int i = 0; i < 200_000; i++) {
+            out.write(emptyPaddingChunk, 0, emptyPaddingChunk.length);
+        }
+        final byte[] input = out.toByteArray();
+        assertDoesNotThrow(() -> {
+            try (FramedSnappyCompressorInputStream in = new FramedSnappyCompressorInputStream(new ByteArrayInputStream(input))) {
+                assertEquals(-1, in.read());
+            }
+        });
+    }
+
     @Test
     void testMatches() throws IOException {
         assertFalse(FramedSnappyCompressorInputStream.matches(new byte[10], 10));
@@ -238,21 +255,4 @@ void testWriteDataLargerThanBufferOneCall() throws IOException {
         assertArrayEquals(data, decompressed);
     }
 
-    @Test
-    void testManyPaddingChunksDoNotOverflowStack() {
-        final byte[] streamIdentifier = { (byte) 0xff, 6, 0, 0, 's', 'N', 'a', 'P', 'p', 'Y' };
-        final byte[] emptyPaddingChunk = { (byte) 0xfe, 0, 0, 0 };
-        final ByteArrayOutputStream out = new ByteArrayOutputStream();
-        out.write(streamIdentifier, 0, streamIdentifier.length);
-        for (int i = 0; i < 200_000; i++) {
-            out.write(emptyPaddingChunk, 0, emptyPaddingChunk.length);
-        }
-        final byte[] input = out.toByteArray();
-        assertDoesNotThrow(() -> {
-            try (FramedSnappyCompressorInputStream in = new FramedSnappyCompressorInputStream(new ByteArrayInputStream(input))) {
-                assertEquals(-1, in.read());
-            }
-        });
-    }
-
 }