Fixes an issue where the CCITTFaxDecoderStream could cause endless reading (and potential OOME)

This commit is contained in:
Harald Kuhr 2024-09-27 10:58:11 +02:00
parent 7fc47a338c
commit f4a5f57d52
3 changed files with 68 additions and 40 deletions

View File

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

View File

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

View File

@ -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