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.
This commit is contained in:
aleister1102
2026-06-07 00:08:16 +07:00
parent f0328d1544
commit 2dc2a5feea
4 changed files with 128 additions and 0 deletions

View File

@@ -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"],
)

View File

@@ -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");
}

View File

@@ -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());
}
}

View File

@@ -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<size_t>(input_length) > handle->input_size) {
return;
}
/* Still have unconsumed data. Workflow is broken. */
if (handle->input_offset < handle->input_length) {
return;