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. // See TIFF 6.0 Specification, Section 10: "Modified Huffman Compression", page 43.
private final int columns; private final int columns;
private int rowsLeft;
private final byte[] decodedRow; private final byte[] decodedRow;
private final boolean optionG32D; private final boolean optionG32D;
@ -70,6 +71,19 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
private int lastChangingElement = 0; 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. * Creates a CCITTFaxDecoderStream.
* This constructor may be used for CCITT streams embedded in PDF files, * 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 options CCITT T.4 or T.6 options.
* @param byteAligned enable byte alignment used in PDF files (EncodedByteAlign). * @param byteAligned enable byte alignment used in PDF files (EncodedByteAlign).
*/ */
public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, final long options, final boolean byteAligned) {
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")); super(Validate.notNull(stream, "stream"));
this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0"); 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 || this.type = Validate.isTrue(type == TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE ||
type == TIFFExtension.COMPRESSION_CCITT_T4 || type == TIFFExtension.COMPRESSION_CCITT_T4 ||
type == TIFFExtension.COMPRESSION_CCITT_T6, type == TIFFExtension.COMPRESSION_CCITT_T6,
type, "Only CCITT Modified Huffman RLE compression (2), CCITT T4 (3) or CCITT T6 (4) supported: %s"); 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) // We know this is only used for b/w (1 bit)
decodedRow = new byte[(columns + 7) / 8]; decodedRow = new byte[(columns + 7) / 8];
@ -122,21 +141,7 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
} }
Validate.isTrue(!optionUncompressed, optionUncompressed, Validate.isTrue(!optionUncompressed, optionUncompressed,
"CCITT GROUP 3/4 OPTION UNCOMPRESSED is not supported"); "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);
} }
static int findCompressionType(final int encodedType, final InputStream stream) throws IOException { static int findCompressionType(final int encodedType, final InputStream stream) throws IOException {
@ -209,9 +214,18 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
throw e; throw e;
} }
// ...otherwise, just let client code try to read past the if (rowsLeft > 0) {
// end of stream // For
decodedLength = -1; 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; decodedPos = 0;
@ -404,7 +418,7 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
int byteIndex = index / 8; int byteIndex = index / 8;
while (index % 8 != 0 && (nextChange - index) > 0) { 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++; index++;
} }
@ -424,7 +438,7 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
decodedRow[byteIndex] = 0; decodedRow[byteIndex] = 0;
} }
decodedRow[byteIndex] |= (white ? 0 : 1 << (7 - ((index) % 8))); decodedRow[byteIndex] |= (byte) (white ? 0 : 1 << (7 - ((index) % 8)));
index++; 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); throw new IOException("Sum of run-lengths does not equal scan line width: " + index + " > " + columns);
} }
rowsLeft--;
decodedLength = (index + 7) / 8; decodedLength = (index + 7) / 8;
} }
@ -494,14 +509,14 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
@Override @Override
public int read() throws IOException { public int read() throws IOException {
if (decodedLength < 0) { if (decodedLength < 0) {
return 0x0; return -1;
} }
if (decodedPos >= decodedLength) { if (decodedPos >= decodedLength) {
fetch(); fetch();
if (decodedLength < 0) { if (decodedLength < 0) {
return 0x0; return -1;
} }
} }
@ -511,16 +526,14 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
@Override @Override
public int read(byte[] b, int off, int len) throws IOException { public int read(byte[] b, int off, int len) throws IOException {
if (decodedLength < 0) { if (decodedLength < 0) {
Arrays.fill(b, off, off + len, (byte) 0x0); return -1;
return len;
} }
if (decodedPos >= decodedLength) { if (decodedPos >= decodedLength) {
fetch(); fetch();
if (decodedLength < 0) { if (decodedLength < 0) {
Arrays.fill(b, off, off + len, (byte) 0x0); return -1;
return len;
} }
} }

View File

@ -1134,7 +1134,7 @@ public final class TIFFImageReader extends ImageReaderBase {
int compressedStripTileWidth = planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR && b > 0 && yCbCrSubsampling != null int compressedStripTileWidth = planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR && b > 0 && yCbCrSubsampling != null
? ((stripTileWidth + yCbCrSubsampling[0] - 1) / yCbCrSubsampling[0]) ? ((stripTileWidth + yCbCrSubsampling[0] - 1) / yCbCrSubsampling[0])
: stripTileWidth; : stripTileWidth;
adapter = createDecompressorStream(compression, compressedStripTileWidth, samplesInTile, adapter); adapter = createDecompressorStream(compression, compressedStripTileWidth, stripTileHeight, samplesInTile, adapter);
adapter = createUnpredictorStream(predictor, compressedStripTileWidth, samplesInTile, bitsPerSample, adapter, imageInput.getByteOrder()); adapter = createUnpredictorStream(predictor, compressedStripTileWidth, samplesInTile, bitsPerSample, adapter, imageInput.getByteOrder());
adapter = createYCbCrUpsamplerStream(interpretation, planarConfiguration, b, rowRaster.getTransferType(), yCbCrSubsampling, yCbCrPos, colsInTile, 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)); 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) { switch (compression) {
case TIFFBaseline.COMPRESSION_NONE: case TIFFBaseline.COMPRESSION_NONE:
return stream; return stream;
@ -2527,7 +2527,7 @@ public final class TIFFImageReader extends ImageReaderBase {
if (overrideCCITTCompression == -1) { if (overrideCCITTCompression == -1) {
overrideCCITTCompression = findCCITTType(compression, stream); 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: default:
throw new IllegalArgumentException("Unsupported TIFF compression: " + compression); throw new IllegalArgumentException("Unsupported TIFF compression: " + compression);
} }

View File

@ -157,8 +157,13 @@ public class CCITTFaxDecoderStreamTest {
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
byte[] bytes = new byte[imageData.length]; byte[] bytes = new byte[imageData.length];
new DataInputStream(stream).readFully(bytes);
DataInputStream dataInputStream = new DataInputStream(stream);
dataInputStream.readFully(bytes);
assertArrayEquals(imageData, bytes); assertArrayEquals(imageData, bytes);
assertEquals(-1, dataInputStream.read());
assertEquals(-1, dataInputStream.read(new byte[1]));
} }
@Test @Test
@ -168,8 +173,13 @@ public class CCITTFaxDecoderStreamTest {
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
byte[] bytes = new byte[imageData.length]; byte[] bytes = new byte[imageData.length];
new DataInputStream(stream).readFully(bytes);
DataInputStream dataInputStream = new DataInputStream(stream);
dataInputStream.readFully(bytes);
assertArrayEquals(imageData, bytes); assertArrayEquals(imageData, bytes);
assertEquals(-1, dataInputStream.read());
assertEquals(-1, dataInputStream.read(new byte[1]));
} }
@Test @Test
@ -179,8 +189,13 @@ public class CCITTFaxDecoderStreamTest {
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData(); byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
byte[] bytes = new byte[imageData.length]; byte[] bytes = new byte[imageData.length];
new DataInputStream(stream).readFully(bytes);
DataInputStream dataInputStream = new DataInputStream(stream);
dataInputStream.readFully(bytes);
assertArrayEquals(imageData, bytes); assertArrayEquals(imageData, bytes);
assertEquals(-1, dataInputStream.read());
assertEquals(-1, dataInputStream.read(new byte[1]));
} }
@Test @Test
@ -265,7 +280,7 @@ public class CCITTFaxDecoderStreamTest {
new DataInputStream(inputStream).readFully(data); new DataInputStream(inputStream).readFully(data);
InputStream stream = new CCITTFaxDecoderStream(new ByteArrayInputStream(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 byte[] bytes = new byte[6]; // 6 x 6 pixel, 1 bpp => 6 bytes
new DataInputStream(stream).readFully(bytes); new DataInputStream(stream).readFully(bytes);
@ -274,8 +289,8 @@ public class CCITTFaxDecoderStreamTest {
byte[] imageData = Arrays.copyOf(((DataBufferByte) image.getData().getDataBuffer()).getData(), 6); byte[] imageData = Arrays.copyOf(((DataBufferByte) image.getData().getDataBuffer()).getData(), 6);
assertArrayEquals(imageData, bytes); 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 @Test