diff --git a/src/main/java/org/xerial/snappy/Snappy.java b/src/main/java/org/xerial/snappy/Snappy.java index 6aeea3e6..7d98705f 100755 --- a/src/main/java/org/xerial/snappy/Snappy.java +++ b/src/main/java/org/xerial/snappy/Snappy.java @@ -87,6 +87,11 @@ static void init() { public static void arrayCopy(Object src, int offset, int byteLength, Object dest, int dest_offset) throws IOException { + if (src == null || dest == null) { + throw new NullPointerException("src or dest is null"); + } + checkArrayRange(src, offset, byteLength, "src"); + checkArrayRange(dest, dest_offset, byteLength, "dest"); impl.arrayCopy(src, offset, byteLength, dest, dest_offset); } @@ -152,7 +157,7 @@ public static int compress(ByteBuffer uncompressed, ByteBuffer compressed) int uPos = uncompressed.position(); int uLen = uncompressed.remaining(); int cPos = compressed.position(); - checkOutputSpace(compressed.remaining(), 0, requiredCompressedLength(uLen)); + checkOutputSpace(compressed.remaining(), 0, maxCompressedLength(uLen)); int compressedSize = impl.rawCompress(uncompressed, uPos, uLen, compressed, cPos); @@ -404,10 +409,21 @@ public static boolean isValidCompressedBuffer(long inputAddr, long offset, long * * @param byteSize byte size of the data to compress * @return maximum byte size of the compressed data + * @throws IllegalArgumentException if byteSize is negative + * @throws SnappyError ({@link SnappyErrorCode#TOO_LARGE_INPUT}) if the maximum compressed size exceeds + * Integer.MAX_VALUE */ public static int maxCompressedLength(int byteSize) { - return impl.maxCompressedLength(byteSize); + if (byteSize < 0) { + throw new IllegalArgumentException("byteSize must not be negative: " + byteSize); + } + int maxCompressedLength = impl.maxCompressedLength(byteSize); + // The native result overflows int for inputs close to 2GB + if (maxCompressedLength < 0) { + throw new SnappyError(SnappyErrorCode.TOO_LARGE_INPUT, "input is too large to compress: " + byteSize); + } + return maxCompressedLength; } /** @@ -454,7 +470,7 @@ public static byte[] rawCompress(Object data, int byteSize) throw new NullPointerException("input is null"); } checkArrayRange(data, 0, byteSize, "input"); - byte[] buf = new byte[requiredCompressedLength(byteSize)]; + byte[] buf = new byte[maxCompressedLength(byteSize)]; int compressedByteSize = impl.rawCompress(data, 0, byteSize, buf, 0); byte[] result = new byte[compressedByteSize]; System.arraycopy(buf, 0, result, 0, compressedByteSize); @@ -482,7 +498,7 @@ public static int rawCompress(Object input, int inputOffset, int inputLength, by throw new NullPointerException("input or output is null"); } checkArrayRange(input, inputOffset, inputLength, "input"); - checkOutputSpace(output.length, outputOffset, requiredCompressedLength(inputLength)); + checkOutputSpace(output.length, outputOffset, maxCompressedLength(inputLength)); int compressedSize = impl .rawCompress(input, inputOffset, inputLength, output, outputOffset); @@ -567,6 +583,22 @@ private static void checkArrayRange(Object array, int offset, int length, String } } + /** + * Ensures the element range [off, off+len) lies within a typed array of arrayLength elements, and that its byte + * offset and length fit in int, before converting them to a byte range. + */ + static void checkElementRange(int arrayLength, int off, int len, int elementSize) + { + if (off < 0 || len < 0 || off > arrayLength - len) { + throw new IndexOutOfBoundsException(String.format( + "range [off:%,d, len:%,d] is out of bounds for array of length %,d", off, len, arrayLength)); + } + if ((long) (off + len) * elementSize > Integer.MAX_VALUE) { + throw new SnappyError(SnappyErrorCode.TOO_LARGE_INPUT, String.format( + "byte size of range [off:%,d, len:%,d] exceeds Integer.MAX_VALUE", off, len)); + } + } + /** * Ensures the output has at least requiredSize bytes available after outputOffset, since the native code writes * the output through a raw pointer without any bounds check. @@ -584,19 +616,6 @@ private static void checkOutputSpace(long outputSize, long outputOffset, long re } } - /** - * Returns the output buffer size required by the native compressor for the given input size. - */ - private static int requiredCompressedLength(int inputLength) - { - int maxCompressedLength = maxCompressedLength(inputLength); - // maxCompressedLength overflows int for inputs close to 2GB - if (maxCompressedLength < 0) { - throw new SnappyError(SnappyErrorCode.TOO_LARGE_INPUT, "input is too large to compress: " + inputLength); - } - return maxCompressedLength; - } - /** * Rejects an uncompressed length declared in a compressed stream header that no valid Snappy stream of the * given compressed size can produce. The densest Snappy element is a 3-byte copy that expands to 64 bytes, so diff --git a/src/main/java/org/xerial/snappy/SnappyInputStream.java b/src/main/java/org/xerial/snappy/SnappyInputStream.java index d6ffcd43..ead44098 100755 --- a/src/main/java/org/xerial/snappy/SnappyInputStream.java +++ b/src/main/java/org/xerial/snappy/SnappyInputStream.java @@ -178,6 +178,7 @@ protected void readFully(byte[] fragment, int fragmentLength) public int read(byte[] b, int byteOffset, int byteLength) throws IOException { + Snappy.checkElementRange(b.length, byteOffset, byteLength, 1); int writtenBytes = 0; for (; writtenBytes < byteLength; ) { @@ -243,6 +244,7 @@ public int rawRead(Object array, int byteOffset, int byteLength) public int read(long[] d, int off, int len) throws IOException { + Snappy.checkElementRange(d.length, off, len, 8); return rawRead(d, off * 8, len * 8); } @@ -273,6 +275,7 @@ public int read(long[] d) public int read(double[] d, int off, int len) throws IOException { + Snappy.checkElementRange(d.length, off, len, 8); return rawRead(d, off * 8, len * 8); } @@ -317,6 +320,7 @@ public int read(int[] d) public int read(int[] d, int off, int len) throws IOException { + Snappy.checkElementRange(d.length, off, len, 4); return rawRead(d, off * 4, len * 4); } @@ -333,6 +337,7 @@ public int read(int[] d, int off, int len) public int read(float[] d, int off, int len) throws IOException { + Snappy.checkElementRange(d.length, off, len, 4); return rawRead(d, off * 4, len * 4); } @@ -363,6 +368,7 @@ public int read(float[] d) public int read(short[] d, int off, int len) throws IOException { + Snappy.checkElementRange(d.length, off, len, 2); return rawRead(d, off * 2, len * 2); } diff --git a/src/main/java/org/xerial/snappy/SnappyOutputStream.java b/src/main/java/org/xerial/snappy/SnappyOutputStream.java index c3a5f386..df96f4da 100755 --- a/src/main/java/org/xerial/snappy/SnappyOutputStream.java +++ b/src/main/java/org/xerial/snappy/SnappyOutputStream.java @@ -119,6 +119,7 @@ public void write(byte[] b, int byteOffset, int byteLength) if (closed) { throw new IOException("Stream is closed"); } + Snappy.checkElementRange(b.length, byteOffset, byteLength, 1); int cursor = 0; while (cursor < byteLength) { int readLen = Math.min(byteLength - cursor, blockSize - inputCursor); @@ -147,6 +148,7 @@ public void write(byte[] b, int byteOffset, int byteLength) public void write(long[] d, int off, int len) throws IOException { + Snappy.checkElementRange(d.length, off, len, 8); rawWrite(d, off * 8, len * 8); } @@ -161,6 +163,7 @@ public void write(long[] d, int off, int len) public void write(double[] f, int off, int len) throws IOException { + Snappy.checkElementRange(f.length, off, len, 8); rawWrite(f, off * 8, len * 8); } @@ -175,6 +178,7 @@ public void write(double[] f, int off, int len) public void write(float[] f, int off, int len) throws IOException { + Snappy.checkElementRange(f.length, off, len, 4); rawWrite(f, off * 4, len * 4); } @@ -189,6 +193,7 @@ public void write(float[] f, int off, int len) public void write(int[] f, int off, int len) throws IOException { + Snappy.checkElementRange(f.length, off, len, 4); rawWrite(f, off * 4, len * 4); } @@ -203,6 +208,7 @@ public void write(int[] f, int off, int len) public void write(short[] f, int off, int len) throws IOException { + Snappy.checkElementRange(f.length, off, len, 2); rawWrite(f, off * 2, len * 2); } diff --git a/src/test/java/org/xerial/snappy/SnappyBoundsCheckTest.java b/src/test/java/org/xerial/snappy/SnappyBoundsCheckTest.java index ea0c017b..238dae45 100644 --- a/src/test/java/org/xerial/snappy/SnappyBoundsCheckTest.java +++ b/src/test/java/org/xerial/snappy/SnappyBoundsCheckTest.java @@ -332,4 +332,91 @@ public void framedStreamWithHugeDeclaredUncompressedLength() assertEquals(SnappyErrorCode.PARSING_ERROR, e.getErrorCode()); } } + + // https://github.com/xerial/snappy-java/issues/671 + @Test + public void maxCompressedLengthOverflow() + throws Exception + { + // 32 + n + n / 6 is Integer.MAX_VALUE for n = 1840700242 + assertEquals(Integer.MAX_VALUE, Snappy.maxCompressedLength(1840700242)); + try { + Snappy.maxCompressedLength(1840700243); + fail("expected SnappyError"); + } + catch (SnappyError e) { + assertEquals(SnappyErrorCode.TOO_LARGE_INPUT, e.errorCode); + } + } + + @Test(expected = IllegalArgumentException.class) + public void maxCompressedLengthOfNegativeSize() + throws Exception + { + Snappy.maxCompressedLength(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void arrayCopyOutOfRange() + throws Exception + { + Snappy.arrayCopy(new int[4], 0, 17, new byte[100], 0); + } + + // https://github.com/xerial/snappy-java/issues/513 + @Test(expected = IllegalArgumentException.class) + public void outputStreamRawWriteOutOfRange() + throws Exception + { + SnappyOutputStream out = new SnappyOutputStream(new ByteArrayOutputStream()); + out.rawWrite(new int[4], 0, 1024); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void outputStreamWriteTypedArrayOutOfRange() + throws Exception + { + SnappyOutputStream out = new SnappyOutputStream(new ByteArrayOutputStream()); + out.write(new long[4], 2, 3); + } + + @Test(expected = SnappyError.class) + public void elementRangeWithOverflowingByteSize() + { + // a long[] range whose byte offset overflows int; called directly to avoid allocating a 2GB array + int length = Integer.MAX_VALUE / 8 + 2; + Snappy.checkElementRange(length, length - 1, 1, 8); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void outputStreamWriteBytesOutOfRange() + throws Exception + { + SnappyOutputStream out = new SnappyOutputStream(new ByteArrayOutputStream()); + out.write(new byte[10], 5, 10); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void inputStreamReadTypedArrayOutOfRange() + throws Exception + { + ByteArrayOutputStream compressed = new ByteArrayOutputStream(); + SnappyOutputStream out = new SnappyOutputStream(compressed); + out.write(new int[100]); + out.close(); + SnappyInputStream in = new SnappyInputStream(new ByteArrayInputStream(compressed.toByteArray())); + in.read(new int[10], 5, 10); + } + + @Test(expected = IllegalArgumentException.class) + public void inputStreamRawReadOutOfRange() + throws Exception + { + ByteArrayOutputStream compressed = new ByteArrayOutputStream(); + SnappyOutputStream out = new SnappyOutputStream(compressed); + out.write(new int[100]); + out.close(); + SnappyInputStream in = new SnappyInputStream(new ByteArrayInputStream(compressed.toByteArray())); + in.rawRead(new int[10], 0, 400); + } }