From f4a5f57d5206a3efa7169267af30a4aaef4e7c57 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Fri, 27 Sep 2024 10:58:11 +0200 Subject: [PATCH] Fixes an issue where the CCITTFaxDecoderStream could cause endless reading (and potential OOME) --- .../plugins/tiff/CCITTFaxDecoderStream.java | 75 +++++++++++-------- .../imageio/plugins/tiff/TIFFImageReader.java | 6 +- .../tiff/CCITTFaxDecoderStreamTest.java | 27 +++++-- 3 files changed, 68 insertions(+), 40 deletions(-) diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStream.java index 6f0ca708..a498c16a 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStream.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStream.java @@ -50,6 +50,7 @@ final class CCITTFaxDecoderStream extends FilterInputStream { // See TIFF 6.0 Specification, Section 10: "Modified Huffman Compression", page 43. private final int columns; + private int rowsLeft; private final byte[] decodedRow; private final boolean optionG32D; @@ -70,6 +71,19 @@ final class CCITTFaxDecoderStream extends FilterInputStream { private int lastChangingElement = 0; + /** + * Creates a CCITTFaxDecoderStream. + * + * @param stream the compressed CCITT stream. + * @param columns the number of columns in the stream. + * @param type the type of stream, must be one of {@code COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE}, + * {@code COMPRESSION_CCITT_T4} or {@code COMPRESSION_CCITT_T6}. + * @param options CCITT T.4 or T.6 options. + */ + public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, final long options) { + this(stream, columns, -1, type, options, type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE); + } + /** * Creates a CCITTFaxDecoderStream. * This constructor may be used for CCITT streams embedded in PDF files, @@ -82,15 +96,20 @@ final class CCITTFaxDecoderStream extends FilterInputStream { * @param options CCITT T.4 or T.6 options. * @param byteAligned enable byte alignment used in PDF files (EncodedByteAlign). */ - public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, - final long options, final boolean byteAligned) { + public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, final long options, final boolean byteAligned) { + this(stream, columns, -1, type, options, byteAligned); + } + + CCITTFaxDecoderStream(final InputStream stream, final int columns, final int rows, final int type, final long options, final boolean byteAligned) { super(Validate.notNull(stream, "stream")); this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0"); + // -1 means as many rows the stream contains, otherwise pad up to 'rows' before EOF for compatibility with legacy CCITT streams + this.rowsLeft = Validate.isTrue(rows == -1 || rows > 0, rows, "height must be greater than 0"); this.type = Validate.isTrue(type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE || - type == TIFFExtension.COMPRESSION_CCITT_T4 || - type == TIFFExtension.COMPRESSION_CCITT_T6, - type, "Only CCITT Modified Huffman RLE compression (2), CCITT T4 (3) or CCITT T6 (4) supported: %s"); + type == TIFFExtension.COMPRESSION_CCITT_T4 || + type == TIFFExtension.COMPRESSION_CCITT_T6, + type, "Only CCITT Modified Huffman RLE compression (2), CCITT T4 (3) or CCITT T6 (4) supported: %s"); // We know this is only used for b/w (1 bit) decodedRow = new byte[(columns + 7) / 8]; @@ -122,21 +141,7 @@ final class CCITTFaxDecoderStream extends FilterInputStream { } Validate.isTrue(!optionUncompressed, optionUncompressed, - "CCITT GROUP 3/4 OPTION UNCOMPRESSED is not supported"); - } - - /** - * Creates a CCITTFaxDecoderStream. - * - * @param stream the compressed CCITT stream. - * @param columns the number of columns in the stream. - * @param type the type of stream, must be one of {@code COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE}, - * {@code COMPRESSION_CCITT_T4} or {@code COMPRESSION_CCITT_T6}. - * @param options CCITT T.4 or T.6 options. - */ - public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, - final long options) { - this(stream, columns, type, options, type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE); + "CCITT GROUP 3/4 OPTION UNCOMPRESSED is not supported"); } static int findCompressionType(final int encodedType, final InputStream stream) throws IOException { @@ -209,9 +214,18 @@ final class CCITTFaxDecoderStream extends FilterInputStream { throw e; } - // ...otherwise, just let client code try to read past the - // end of stream - decodedLength = -1; + if (rowsLeft > 0) { + // For + rowsLeft--; + + Arrays.fill(decodedRow, (byte) 0); + decodedLength = decodedRow.length; + } + else { + // ...otherwise, just let client code try to read past the + // end of stream + decodedLength = -1; + } } decodedPos = 0; @@ -404,7 +418,7 @@ final class CCITTFaxDecoderStream extends FilterInputStream { int byteIndex = index / 8; while (index % 8 != 0 && (nextChange - index) > 0) { - decodedRow[byteIndex] |= (white ? 0 : 1 << (7 - ((index) % 8))); + decodedRow[byteIndex] |= (byte) (white ? 0 : 1 << (7 - ((index) % 8))); index++; } @@ -424,7 +438,7 @@ final class CCITTFaxDecoderStream extends FilterInputStream { decodedRow[byteIndex] = 0; } - decodedRow[byteIndex] |= (white ? 0 : 1 << (7 - ((index) % 8))); + decodedRow[byteIndex] |= (byte) (white ? 0 : 1 << (7 - ((index) % 8))); index++; } @@ -435,6 +449,7 @@ final class CCITTFaxDecoderStream extends FilterInputStream { throw new IOException("Sum of run-lengths does not equal scan line width: " + index + " > " + columns); } + rowsLeft--; decodedLength = (index + 7) / 8; } @@ -494,14 +509,14 @@ final class CCITTFaxDecoderStream extends FilterInputStream { @Override public int read() throws IOException { if (decodedLength < 0) { - return 0x0; + return -1; } if (decodedPos >= decodedLength) { fetch(); if (decodedLength < 0) { - return 0x0; + return -1; } } @@ -511,16 +526,14 @@ final class CCITTFaxDecoderStream extends FilterInputStream { @Override public int read(byte[] b, int off, int len) throws IOException { if (decodedLength < 0) { - Arrays.fill(b, off, off + len, (byte) 0x0); - return len; + return -1; } if (decodedPos >= decodedLength) { fetch(); if (decodedLength < 0) { - Arrays.fill(b, off, off + len, (byte) 0x0); - return len; + return -1; } } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java index 3f305887..a4d33c7c 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java @@ -1134,7 +1134,7 @@ public final class TIFFImageReader extends ImageReaderBase { int compressedStripTileWidth = planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR && b > 0 && yCbCrSubsampling != null ? ((stripTileWidth + yCbCrSubsampling[0] - 1) / yCbCrSubsampling[0]) : stripTileWidth; - adapter = createDecompressorStream(compression, compressedStripTileWidth, samplesInTile, adapter); + adapter = createDecompressorStream(compression, compressedStripTileWidth, stripTileHeight, samplesInTile, adapter); adapter = createUnpredictorStream(predictor, compressedStripTileWidth, samplesInTile, bitsPerSample, adapter, imageInput.getByteOrder()); adapter = createYCbCrUpsamplerStream(interpretation, planarConfiguration, b, rowRaster.getTransferType(), yCbCrSubsampling, yCbCrPos, colsInTile, adapter, imageInput.getByteOrder()); @@ -2506,7 +2506,7 @@ public final class TIFFImageReader extends ImageReaderBase { return (short) Math.max(0, Math.min(0xffff, val)); } - private InputStream createDecompressorStream(final int compression, final int width, final int bands, InputStream stream) throws IOException { + private InputStream createDecompressorStream(final int compression, final int width, final int height, final int bands, InputStream stream) throws IOException { switch (compression) { case TIFFBaseline.COMPRESSION_NONE: return stream; @@ -2527,7 +2527,7 @@ public final class TIFFImageReader extends ImageReaderBase { if (overrideCCITTCompression == -1) { overrideCCITTCompression = findCCITTType(compression, stream); } - return new CCITTFaxDecoderStream(stream, width, overrideCCITTCompression, getCCITTOptions(compression), compression == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE); + return new CCITTFaxDecoderStream(stream, width, height, overrideCCITTCompression, getCCITTOptions(compression), compression == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE); default: throw new IllegalArgumentException("Unsupported TIFF compression: " + compression); } diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStreamTest.java index 10730e95..e4bf7c74 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStreamTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStreamTest.java @@ -157,8 +157,13 @@ public class CCITTFaxDecoderStreamTest { byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); byte[] bytes = new byte[imageData.length]; - new DataInputStream(stream).readFully(bytes); + + DataInputStream dataInputStream = new DataInputStream(stream); + dataInputStream.readFully(bytes); assertArrayEquals(imageData, bytes); + + assertEquals(-1, dataInputStream.read()); + assertEquals(-1, dataInputStream.read(new byte[1])); } @Test @@ -168,8 +173,13 @@ public class CCITTFaxDecoderStreamTest { byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); byte[] bytes = new byte[imageData.length]; - new DataInputStream(stream).readFully(bytes); + + DataInputStream dataInputStream = new DataInputStream(stream); + dataInputStream.readFully(bytes); assertArrayEquals(imageData, bytes); + + assertEquals(-1, dataInputStream.read()); + assertEquals(-1, dataInputStream.read(new byte[1])); } @Test @@ -179,8 +189,13 @@ public class CCITTFaxDecoderStreamTest { byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); byte[] bytes = new byte[imageData.length]; - new DataInputStream(stream).readFully(bytes); + + DataInputStream dataInputStream = new DataInputStream(stream); + dataInputStream.readFully(bytes); assertArrayEquals(imageData, bytes); + + assertEquals(-1, dataInputStream.read()); + assertEquals(-1, dataInputStream.read(new byte[1])); } @Test @@ -265,7 +280,7 @@ public class CCITTFaxDecoderStreamTest { new DataInputStream(inputStream).readFully(data); InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(data), - 6, TIFFExtension.COMPRESSION_CCITT_T6, 0L); + 6, 6, TIFFExtension.COMPRESSION_CCITT_T6, 0L, false); byte[] bytes = new byte[6]; // 6 x 6 pixel, 1 bpp => 6 bytes new DataInputStream(stream).readFully(bytes); @@ -274,8 +289,8 @@ public class CCITTFaxDecoderStreamTest { byte[] imageData = Arrays.copyOf(((DataBufferByte) image.getData().getDataBuffer()).getData(), 6); assertArrayEquals(imageData, bytes); - // Ideally, we should have no more data now, but the stream don't know that... - // assertEquals("Should contain no more data", -1, stream.read()); + assertEquals("Should contain no more data", -1, stream.read()); + assertEquals("Should contain no more data", -1, stream.read(new byte[1])); } @Test