diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index af7d2d9f525..0b3f019bc78 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -805,6 +805,41 @@ def test_save_bytesio_encoder_error(self) -> None: tiffinfo={TiffImagePlugin.PREDICTOR: 3}, ) + def test_save_bytesio_seek_past_end(self, tmp_path: Path) -> None: + # When writing the directory, libtiff seeks past the end of the data + # written so far, and then writes the tag values there. Check that the + # in-memory write buffer grows to fit, and that the bytes that are + # skipped over are zero, as they would be when writing to a file. + + # 7 distinct bytes per row: PackBits emits 8 bytes per row, so the strip + # plus the header are exactly 64 KiB, the initial size of the buffer + im = Image.new("L", (7, 8191)) + im.putdata([(i * 37) & 0xFF for i in range(7 * 8191)]) + kwargs: dict[str, Any] = { + "compression": "packbits", + "description": "D" * 65535, + } + + outfile = tmp_path / "temp.tif" + im.save(outfile, **kwargs) + buffer_io = io.BytesIO() + im.save(buffer_io, format="tiff", **kwargs) + assert buffer_io.getvalue() == outfile.read_bytes() + + # odd-length tag values, so that libtiff pads between them. Large enough + # that the buffer has to be reallocated + im = hopper().resize((300, 300)) + kwargs = { + "compression": "tiff_lzw", + "description": "abcdef", + "software": "xyz0", + "tiffinfo": {315: "artist", 33432: "copyright!"}, + } + im.save(outfile, **kwargs) + buffer_io = io.BytesIO() + im.save(buffer_io, format="tiff", **kwargs) + assert buffer_io.getvalue() == outfile.read_bytes() + def test_save_ycbcr(self, tmp_path: Path) -> None: im = hopper("YCbCr") outfile = tmp_path / "temp.tif" diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 67f11b40901..b40bda2eb5d 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -67,11 +67,13 @@ _tiffWriteProc(thandle_t hdata, tdata_t buf, tsize_t size) { TIFFSTATE *state = (TIFFSTATE *)hdata; tsize_t to_write; + // libtiff may have seeked past the end of the buffer, e.g. when writing the + // directory, so the required size is based on the current location. to_write = MIN(size, state->size - (tsize_t)state->loc); if (state->flrealloc && size > to_write) { tdata_t new_data; tsize_t newsize = state->size; - while (newsize < (size + state->size)) { + while (newsize < (size + (tsize_t)state->loc)) { if (newsize > INT_MAX - 64 * 1024) { return 0; } @@ -84,9 +86,20 @@ _tiffWriteProc(thandle_t hdata, tdata_t buf, tsize_t size) { // fail out return 0; } + // zero the new space, so that any bytes libtiff seeks over without + // writing are 0 in the output, as they would be in a file + memset((UINT8 *)new_data + state->size, 0, newsize - state->size); state->data = new_data; state->size = newsize; to_write = size; + } else if (to_write < 0) { + TIFFError( + "_tiffWriteProc", + "Invalid Write at loc %" PRIu64 ", size: %" PRIu64, + state->loc, + (toff_t)state->size + ); + return 0; } _TIFFmemcpy((UINT8 *)state->data + state->loc, buf, to_write);