From e28adbc7958673c257f840933d29511b7507343d Mon Sep 17 00:00:00 2001 From: iliasabk <286468824+iliasabk@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:43:56 +0200 Subject: [PATCH] Consume skippable framed chunks iteratively in ensureBuffer Each reserved-skippable chunk made ensureBuffer() recurse into itself, so a crafted stream added one call-stack frame per ~4 input bytes and terminated reads with StackOverflowError. Skipping iteratively keeps the stack depth constant regardless of how many chunks precede the next data frame. Fixes xerial/snappy-java#731 Signed-off-by: Ilias Aberkane --- .../snappy/SnappyFramedInputStream.java | 22 +++++--- .../xerial/snappy/SnappyFramedStreamTest.java | 50 +++++++++++++++++++ 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/xerial/snappy/SnappyFramedInputStream.java b/src/main/java/org/xerial/snappy/SnappyFramedInputStream.java index 8e975735..d4b8d637 100644 --- a/src/main/java/org/xerial/snappy/SnappyFramedInputStream.java +++ b/src/main/java/org/xerial/snappy/SnappyFramedInputStream.java @@ -517,18 +517,24 @@ private boolean ensureBuffer() return false; } - if (!readBlockHeader()) { - eof = true; - return false; - } + // consume any run of skippable chunks iteratively; recursing once per + // chunk lets ~4 input bytes grow the call stack by one frame + FrameMetaData frameMetaData; + while (true) { + if (!readBlockHeader()) { + eof = true; + return false; + } - // get action based on header - final FrameMetaData frameMetaData = getFrameMetaData(frameHeader); + // get action based on header + frameMetaData = getFrameMetaData(frameHeader); + + if (FrameAction.SKIP != frameMetaData.frameAction) { + break; + } - if (FrameAction.SKIP == frameMetaData.frameAction) { SnappyFramed.skip(rbc, frameMetaData.length, ByteBuffer.wrap(buffer)); - return ensureBuffer(); } if (frameMetaData.length > input.capacity()) { diff --git a/src/test/java/org/xerial/snappy/SnappyFramedStreamTest.java b/src/test/java/org/xerial/snappy/SnappyFramedStreamTest.java index e428bf98..815aebfc 100644 --- a/src/test/java/org/xerial/snappy/SnappyFramedStreamTest.java +++ b/src/test/java/org/xerial/snappy/SnappyFramedStreamTest.java @@ -163,6 +163,56 @@ public void testSkippableChunkFlags() } } + @Test + public void testSkippableChunkRun() + throws Exception + { + // each zero-length skippable chunk is only 4 bytes on the wire; a long + // run must be skipped iteratively without exhausting the call stack + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + stream.write(HEADER_BYTES); + final byte[] skipChunk = {(byte) 0x80, 0, 0, 0}; + for (int i = 0; i < 100000; i++) { + stream.write(skipChunk); + } + // uncompressed data chunk: flag 0x01, length 6 (crc32c + 2 bytes) + final byte[] data = {'h', 'i'}; + final int crc32c = maskedCrc32c(data); + stream.write(new byte[] {1, 6, 0, 0, (byte) crc32c, + (byte) (crc32c >>> 8), (byte) (crc32c >>> 16), + (byte) (crc32c >>> 24), 'h', 'i'}); + + InputStream in = createInputStream(new ByteArrayInputStream( + stream.toByteArray()), true); + try { + assertArrayEquals(new byte[] {'h', 'i'}, toByteArray(in)); + } + finally { + in.close(); + } + } + + @Test + public void testSkippableChunkRunToEof() + throws Exception + { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + stream.write(HEADER_BYTES); + final byte[] skipChunk = {(byte) 0x80, 0, 0, 0}; + for (int i = 0; i < 100000; i++) { + stream.write(skipChunk); + } + + InputStream in = createInputStream(new ByteArrayInputStream( + stream.toByteArray()), true); + try { + assertEquals(-1, in.read()); + } + finally { + in.close(); + } + } + @Test(expected = IOException.class) public void testInvalidBlockSizeZero() throws Exception