From 2dc2a5feeaf3fc2f3e46ce8e20be1d296dd6d485 Mon Sep 17 00:00:00 2001 From: aleister1102 Date: Sun, 7 Jun 2026 00:08:16 +0700 Subject: [PATCH] fix(java): reject decoder push length exceeding input buffer capacity DecoderJNI.Wrapper.push(int length) validated only length < 0. nativePush() stored input_length without an upper bound, and DecoderHandle did not retain the input buffer's allocation size, so new DecoderJNI.Wrapper(1).push(32) made BrotliDecoderDecompressStream read past the one-byte input allocation (heap out-of-bounds read). Add an upper-bound check in Wrapper.push(), and as defense in depth store the allocation size in DecoderHandle and reject oversized input_length in nativePush(). Add DecoderJNITest covering rejection and a valid round trip. --- java/org/brotli/wrapper/dec/BUILD.bazel | 13 +++ java/org/brotli/wrapper/dec/DecoderJNI.java | 3 + .../brotli/wrapper/dec/DecoderJNITest.java | 102 ++++++++++++++++++ java/org/brotli/wrapper/dec/decoder_jni.cc | 10 ++ 4 files changed, 128 insertions(+) create mode 100644 java/org/brotli/wrapper/dec/DecoderJNITest.java diff --git a/java/org/brotli/wrapper/dec/BUILD.bazel b/java/org/brotli/wrapper/dec/BUILD.bazel index e4edcf9..db7b341 100644 --- a/java/org/brotli/wrapper/dec/BUILD.bazel +++ b/java/org/brotli/wrapper/dec/BUILD.bazel @@ -104,3 +104,16 @@ java_test( test_class = "org.brotli.wrapper.dec.CornerCasesTest", runtime_deps = [":test_lib"], ) + +java_test( + name = "DecoderJNITest", + size = "large", + data = [ + ":brotli_jni", # Bazel JNI workaround + ], + jvm_flags = [ + "-DBROTLI_JNI_LIBRARY=$(location :brotli_jni)", + ], + test_class = "org.brotli.wrapper.dec.DecoderJNITest", + runtime_deps = [":test_lib"], +) diff --git a/java/org/brotli/wrapper/dec/DecoderJNI.java b/java/org/brotli/wrapper/dec/DecoderJNI.java index 726a6d6..01e07ba 100644 --- a/java/org/brotli/wrapper/dec/DecoderJNI.java +++ b/java/org/brotli/wrapper/dec/DecoderJNI.java @@ -58,6 +58,9 @@ public class DecoderJNI { if (length < 0) { throw new IllegalArgumentException("negative block length"); } + if (length > inputBuffer.capacity()) { + throw new IllegalArgumentException("block length exceeds input buffer capacity"); + } if (context[0] == 0) { throw new IllegalStateException("brotli decoder is already destroyed"); } diff --git a/java/org/brotli/wrapper/dec/DecoderJNITest.java b/java/org/brotli/wrapper/dec/DecoderJNITest.java new file mode 100644 index 0000000..99faf8f --- /dev/null +++ b/java/org/brotli/wrapper/dec/DecoderJNITest.java @@ -0,0 +1,102 @@ +/* Copyright 2025 Google Inc. All Rights Reserved. + + Distributed under MIT license. + See file LICENSE for detail or copy at https://opensource.org/licenses/MIT +*/ + +package org.brotli.wrapper.dec; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.brotli.integration.BrotliJniTestBase; +import org.brotli.wrapper.enc.Encoder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link org.brotli.wrapper.dec.DecoderJNI}. */ +@RunWith(JUnit4.class) +public class DecoderJNITest extends BrotliJniTestBase { + + @Test + public void testPushLengthExceedingInputBufferIsRejected() throws IOException { + DecoderJNI.Wrapper decoder = new DecoderJNI.Wrapper(1); + try { + decoder.getInputBuffer().put(0, (byte) 0); + try { + decoder.push(2); + fail("push(length > inputBufferSize) must be rejected"); + } catch (IllegalArgumentException expected) { + // Expected: oversized length would read past the input buffer. + } + } finally { + decoder.destroy(); + } + } + + @Test + public void testPushNegativeLengthIsRejected() throws IOException { + DecoderJNI.Wrapper decoder = new DecoderJNI.Wrapper(4); + try { + try { + decoder.push(-1); + fail("push(negative) must be rejected"); + } catch (IllegalArgumentException expected) { + // Expected. + } + } finally { + decoder.destroy(); + } + } + + @Test + public void testPushLengthEqualToInputBufferRoundTrips() throws IOException { + byte[] data = new byte[64]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (i * 7); + } + byte[] compressed = Encoder.compress(data); + + DecoderJNI.Wrapper decoder = new DecoderJNI.Wrapper(compressed.length); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try { + decoder.getInputBuffer().put(compressed); + decoder.push(compressed.length); + int iterations = 0; + while (decoder.getStatus() != DecoderJNI.Status.DONE) { + if (++iterations > 1000) { + fail("decoder did not finish within iteration bound"); + } + switch (decoder.getStatus()) { + case OK: + decoder.push(0); + break; + + case NEEDS_MORE_OUTPUT: + ByteBuffer buffer = decoder.pull(); + byte[] chunk = new byte[buffer.remaining()]; + buffer.get(chunk); + output.write(chunk, 0, chunk.length); + break; + + case NEEDS_MORE_INPUT: + decoder.push(0); + if (decoder.getStatus() == DecoderJNI.Status.NEEDS_MORE_INPUT) { + fail("unexpected truncated stream"); + } + break; + + default: + fail("unexpected decoder status: " + decoder.getStatus()); + } + } + } finally { + decoder.destroy(); + } + assertArrayEquals(data, output.toByteArray()); + } +} diff --git a/java/org/brotli/wrapper/dec/decoder_jni.cc b/java/org/brotli/wrapper/dec/decoder_jni.cc index a92d34c..1004bab 100644 --- a/java/org/brotli/wrapper/dec/decoder_jni.cc +++ b/java/org/brotli/wrapper/dec/decoder_jni.cc @@ -24,6 +24,7 @@ typedef struct DecoderHandle { size_t dictionary_count; uint8_t* input_start; + size_t input_size; size_t input_offset; size_t input_length; } DecoderHandle; @@ -67,6 +68,7 @@ JNIEXPORT jobject JNICALL Java_org_brotli_wrapper_dec_DecoderJNI_nativeCreate( handle->dictionary_count = 0; handle->input_offset = 0; handle->input_length = 0; + handle->input_size = 0; handle->input_start = nullptr; if (input_size == 0) { @@ -74,6 +76,9 @@ JNIEXPORT jobject JNICALL Java_org_brotli_wrapper_dec_DecoderJNI_nativeCreate( } else { handle->input_start = new (std::nothrow) uint8_t[input_size]; ok = !!handle->input_start; + if (ok) { + handle->input_size = input_size; + } } } @@ -125,6 +130,11 @@ JNIEXPORT void JNICALL Java_org_brotli_wrapper_dec_DecoderJNI_nativePush( env->functions->SetLongArrayRegion(env, ctx, 0, 3, context); if (input_length != 0) { + /* Reject input length that exceeds the allocated input buffer. */ + if (input_length < 0 || + static_cast(input_length) > handle->input_size) { + return; + } /* Still have unconsumed data. Workflow is broken. */ if (handle->input_offset < handle->input_length) { return;