mirror of
https://github.com/google/brotli.git
synced 2026-07-08 17:56:58 +00:00
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:
@@ -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"],
|
||||
)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
102
java/org/brotli/wrapper/dec/DecoderJNITest.java
Normal file
102
java/org/brotli/wrapper/dec/DecoderJNITest.java
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user