From 78c08bd4d9c08037f77465a61d90633f669013f4 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Mon, 12 Oct 2026 21:18:41 -0400 Subject: [PATCH] Fix heap buffer overflow in _tiffWriteProc when saving TIFF to BytesIO libtiff seeks past the current end of data when writing the directory and writes tag values there. _tiffWriteProc sized its realloc() from state->size + size instead of the write position state->loc + size, so the first out-of-line tag value written after such a seek could overrun the allocation by up to the size of the directory. The bytes skipped by the seek were also never zeroed, leaking stale heap contents into the saved file. Size the buffer from the write position and zero the space added by realloc, matching the semantics of writing to a real file. Reject writes where the position is still beyond the buffer for the non-reallocating case. Reported-by: carlini Reported-by: Tomer-PL Fixes: GHSA-332p-7hrc-92wf Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- Tests/test_file_libtiff.py | 35 +++++++++++++++++++++++++++++++++++ src/libImaging/TiffDecode.c | 15 ++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) 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);