Repository navigation
Update dependency pillow to v13 [SECURITY] - #896
Open
renovate[bot] wants to merge 1 commit into
Open
renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
This PR body was truncated due to platform limits.
This PR contains the following updates:
>=12.0.0,<13.0.0→>=13.0.0,<13.1.0Pillow: OOB read in TiffDecode.c tile and RGBA paths leaks heap memory into pixels
CVE-2026-112404 / GHSA-c6hw-5grf-ff96
More information
Details
Pillow reads past the end of a heap buffer when decoding certain malformed compressed TIFF files, and copies the bytes it reads into the pixels of the image it returns. An attacker who can get an application to open and load an image file, for example by uploading it to a service that returns thumbnails, previews or converted images, gets back fragments of that process's heap memory, including pointer values. A slightly larger file instead crashes the process. No special settings are needed, and nothing is written out of bounds.
The bug is in
_decodeTile()and_decodeAsRGBA()insrc/libImaging/TiffDecode.c. Both size their decode buffer from libtiff's view of the pixel layout (TIFFTileSize()/TIFFTileRowSize(), orimg.width * 4). They then unpack each row with the unpacker thatTiffImagePlugin._setup()chose from Pillow's own parse of the directory, and neither checks that the unpacker's input row fits inside the row libtiff produced._decodeStrip()has exactly this check, added for OSS-Fuzzcrash-0da0in 8.2.0, but the tile and RGBA paths never got it.The two parsers can be made to disagree with a duplicated
PhotometricInterpretationorBitsPerSampletag, because libtiff keeps the first occurrence and Pillow keeps the last. Pillow then unpacks 4 to 8 bytes per pixel from rows that libtiff filled with 1 to 4 bytes, so the last rows of each tile are read from beyond the buffer. The disclosed memory is a few tile rows past the buffer, and the attacker chooses the tile width. The amount does not grow with the number of tiles, because the buffer is reused for every tile.The tile half was introduced in 1c295bf43 (first released in 8.2.0). The RGBA half was introduced in eb8c1206d, the CVE-2020-35654 fix (8.1.0).
The attached reproducer runs against an unmodified build. It writes three roughly 400-byte files that load without error, yet the last pixel rows of each change with glibc's malloc fill pattern and contain heap pointer bytes. A fourth file with a 65536x16 tile segfaults in
copy4←_decodeTile(TiffDecode.c:483) in most runs, depending on what is mapped after the buffer.Details
_decodeTile()and_decodeAsRGBA()insrc/libImaging/TiffDecode.csizestate->bufferfrom what libtiff says it will produce (TIFFTileSize(), resp.img.width * 4), but unpack each row with the shuffler thatTiffImagePlugin._setup()selected from Pillow's own parse of the IFD. Nothing in either function checks that the unpacker's input row (width * state->bits / 8bytes) fits inside the row libtiff wrote._decodeStrip()has exactly that check; the other two do not. Whenever the two IFD parsers disagree about the pixel layout — a tag with the wrong count or type that libtiff ignores but Pillow accepts, or a duplicatedPhotometricInterpretation/BitsPerSample/SamplesPerPixelentry (libtiff keeps the first occurrence,ImageFileDirectory_v2.load()the last) — the unpacker reads past the end of the heap buffer and the bytes it reads become pixels of the returned image. Any Pillow rawmode wider than libtiff's actual sample layout works (RGBX,RGB;16N,RGBA;16N,I;32,F;32F, …);RGBA;16N(64 bits/pixel) against 1-bit libtiff data is the largest ratio, where every row after the first of each tile is read almost entirely from beyond the buffer.The ~400-byte TIFF
tile_count2.tifgenerated by the reproducer — one 16x16 tile,BitsPerSample 8,SamplesPerPixel 1,Compression 8(Adobe Deflate), andPhotometricInterpretationwritten as a SHORT with count 2 (6, 6) — is opened byImage.open()as modeRGBwith libtiff rawmodeRGBX, andload()succeeds on the stock build. Rows 13–15 of the result contain bytes that lie 16, 32 and 48 bytes past the 256-byte tile buffer: decoding the same file twice underMALLOC_PERTURB_=85and=170yields identical rows 0–12 and different bytes in rows 13–15 (aavs55, i.e. glibc's fill pattern). Under an ASan build of the extensions the sameload()reportsheap-buffer-overflow READ of size 64incopy4(src/libImaging/Unpack.c:1290) called fromsrc/libImaging/TiffDecode.c:483, against the 256-byte region allocated atTiffDecode.c:448. The same layout with a single 65536x16 tile (4.5 KB file) makes the stock build SIGSEGV incopy4 ← _decodeTilereading an unmapped address in most runs (whether it faults depends on what is mapped after the 1 MiB buffer; a wider tile makes it reliable).Why the two parsers disagree
libtiff 4.5 rejects a
PhotometricInterpretationentry whose count is not 1 (TIFFFetchNormalTag: Incorrect count for "PhotometricInterpretation"; tag ignored), so for this fileTIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, …)returns 0 and libtiff treats the data as one 8-bit sample per pixel:TIFFTileSize() = 256,TIFFTileRowSize() = 16.Pillow keeps the first value and carries on:
so
_setup()seesphoto == 6.OPEN_INFO[(II, 6, (1,), 1, (8,), ())]is("L", "L"), and because the file is compressed the libtiff branch then overrides the rawmode:RGBXis a 32-bit rawmode, soget_unpacker()(src/decode.c:301-309) setsstate->bits = 32andstate->shuffle = copy4. The intent of thephoto == 6override is that libtiff will deliver RGBA throughTIFFRGBAImage, but that decision is made in C from libtiff's reading of the tag, which here is "not YCbCr":The missing check
_decodeTile()only guards against libtiff's tile being larger than what the unpacker expects, then allocates exactlyTIFFTileSize()bytes and feedsrow_byte_size-strided rows to the shuffler:Row
tile_yis read from[16·tile_y, 16·tile_y + 64); fortile_y= 13, 14, 15 that ends 16, 32, 48 bytes past the 256-byte allocation. In general the last rows of every tile over-read by up to(state->bits / libtiff_bits_per_pixel − 1) × row_byte_sizebytes, androw_byte_sizescales with the attacker-chosenTileWidth.Note also that the existing one-sided check at line 434 multiplies the file-controlled
tile_width/tile_length(UINT32, accepted up toINT_MAXat line 428) bystate->bitsin 32-bit unsigned arithmetic, sotile_width * state->bitswraps fortile_width ≥ 2^32 / bits(2^26 for the 64-bit rawmodes, 2^27 forRGBX/I;32/F;32F). An 8 KB file declaring a 2,097,152×1 image withTILEWIDTH = 2^26+16,TILELENGTH = 1, 1-bit data for libtiff andRGBA;16Nfor Pillow makes libtiff allocate an ~8 MB tile from whichunpackRGBA16Lreads 16 MB, and the stock build SIGSEGVs inunpackRGBA16L ← _decodeTile (TiffDecode.c:483); a row-size comparison written in the same 32-bit arithmetic as line 434 would evaluate that row to 128 bytes and still accept it, so the new check must be done in a 64-bit type.Compare the strip path, which refuses this situation (added for OSS-Fuzz
crash-0da0in 8.2.0):The identical file written with one strip instead of one tile fails
load()withOSError: decoder error -2because of this check.Same gap in
_decodeAsRGBA()_decodeAsRGBA()always producesimg.width * 4bytes per row (TiffDecode.c:325, buffer allocated at line 339) and unpacks withstate->shuffle(line 373) without looking atstate->bits. It is reached when libtiff sees YCbCr while Pillow does not forceRGBX. libtiff marks later duplicates of a tag as ignored (first occurrence wins); Pillow'sImageFileDirectory_v2.load()overwrites (TiffImagePlugin.py:945, last wins). The reproducer'sycbcr_dup.tifis a 16x16 Deflate strip withPhotometricInterpretationpresent twice (6, then2) andBitsPerSampletwice (8,8,8, then16,16,16): libtiff decodes 8-bit YCbCr into a 1024-byte RGBA raster, Pillow selectsRGB;16N(state->bits = 48), andunpackRGB16Lreads 96 bytes per 64-byte row — ASan reportsheap-buffer-overflowinunpackRGB16L(Unpack.c:609) fromTiffDecode.c:373, past the 1024-byte region fromTiffDecode.c:339.The count-2 encoding is not the only lever, and on released versions it is not a lever at all: a 1-sample
photo == 6file only gets rawmodeRGBXsince the unreleased commit 155330d63 ("Do not open YCbCr colorspace images as RGB without libtiff (#9779)", 2026-08-26). Released versions are reached through duplicated tags instead. A 3-sample tiled file with twoPhotometricInterpretationentries (1or2, then6) produces the samecopy4←_decodeTileover-read, since libtiff keeps the first value and decodes 3 bytes per pixel while Pillow keeps6and unpacks 4 (the reproducer'stile_dup.tif).ycbcr_dup.tifabove reaches_decodeAsRGBA()the same way.Impact
Any application that calls
Image.open()+load()(orthumbnail(),convert(),save(), …) on an untrusted file reaches this with default settings: the file is content-sniffed as TIFF, and any compressed TIFF is decoded through libtiff andTiffDecode.cwithoutREAD_LIBTIFFbeing set. The over-read bytes are copied into the image's pixel rows, so a service that returns a thumbnail, preview or converted image hands adjacent heap memory back to the uploader, heap pointer values included — up to three libtiff tile rows past the buffer for theRGBX-over-8-bit case, with the tile row length (TileWidth) chosen by the attacker.state->bufferis allocated once and reused for every tile, so each tile re-reads the same region past that one buffer; the distinct heap disclosed per image is about(unpacker_bpp / libtiff_bpp − 1)tile rows and does not grow with the tile count. With a wide tile the read runs off the mapping and the process segfaults instead (the 65536x16 case crashes the stock build in most runs, depending on what is mapped after the buffer), taking down the whole worker rather than one request. Writes go to the correctly sizedImagingrows, so this is disclosure / crash, not memory corruption.This is the same family as CVE-2020-35654 (Pillow and libtiff interpreting a YCbCr IFD differently in
TiffDecode.c); that fix and the latercrash-0da0fix closed the write and the strip-path read, but the tile path and the RGBA path still trust Pillow's rawmode over libtiff's row size. The tile-path gap was introduced in 1c295bf43c3a ("Check for dimensions and sizes to fit into int", 2021-01-25), which switched_decodeTile()from a buffer sized by Pillow's unpacker toTIFFTileSize()/TIFFTileRowSize(); it is first in Pillow 8.2.0. The_decodeAsRGBA()half dates from eb8c1206d6b1 ("Fix CVE-2020-35654 - OOB Write in TiffDecode.c"), first in 8.1.0.Reproducing
Run
repro(a single Python 3 script) against the installed, unmodified Pillow. No sanitizer build is needed. It writes four TIFFs into a temporary directory, decodes each one in a child process, and removes the directory at the end.The script writes its TIFFs byte by byte with a small classic-TIFF writer. Each file has a single IFD and a single Deflate-compressed block. Deflate is only there so the file is decoded by libtiff and
TiffDecode.c. Every pixel byte in the compressed data is defined, so the decoded image should not depend on anything else in the process:tile_count2.tif: a 16x16, 1-sample, 8-bit tile withPhotometricInterpretationwritten as SHORT count 2,(6, 6). Pillow keeps6and unpacks withcopy4(RGBX, 4 bytes per pixel). libtiff ignores the tag and decodes 1 byte per pixel. This lever needs 155330d63 (unreleased).tile_dup.tif: a 16x16, 3-sample, 8-bit tile withPhotometricInterpretationpresent twice,2then6. libtiff keeps2(RGB, 3 bytes per pixel) and Pillow keeps6(RGBX, 4 bytes per pixel). This variant also applies to released versions.ycbcr_dup.tif: the 16x16 strip withBitsPerSample8,8,8then16,16,16andPhotometricInterpretation6then2. libtiff decodes 8-bit YCbCr through_decodeAsRGBA()(4 bytes per pixel), and Pillow unpacksRGB;16NwithunpackRGB16L(6 bytes per pixel).tile_big.tif: thetile_count2.tiflayout with a single 65536x16 tile.For each of the first three files, the script runs
Image.open()+load()+tobytes()twice, once withMALLOC_PERTURB_=85and once with=170. If the two outputs differ, bytes from beyond the decode buffer have become pixels. In that case it prints the first differing offset and the bytes around it from both runs. On this tree all three differ in their last rows: theaa/55fill bytes show up, along with heap pointer bytes. The script then decodestile_big.tifand checks for a SIGSEGV (exit status -11). Ifgdbis available, it prints the top frames:copy4(Unpack.c:1290) ←_decodeTile(TiffDecode.c:483) ←ImagingLibTiffDecode. This crash depends on ASLR, so an occasional run may survive.Each step prints which
_imagingbuild it used and how Pillow opened the file (mode, size and rawmode). The script ends with a one-line summary. The last line isBUGif any file leaked heap bytes into its pixels or the big tile crashed the process. Otherwise it isOK.Suggested fix
The attached patch restores the
_decodeStrip()invariant (the unpacker's input row must fit inside libtiff's row) in the two sibling decoders insrc/libImaging/TiffDecode.c, at the point where each one sizes its buffer:_decodeTile()computesunpacker_row_byte_size = ((tsize_t)tile_width * state->bits / planes + 7) / 8(thetsize_tcast keeps the product out of 32-bit arithmetic, so theTILEWIDTH = 2^26+16case above cannot wrap past the check on 64-bit builds). It reuses that value for the existingTIFFTileSize()sanity check, as_decodeStrip()does for strips. After that check and before therealloc, it fails withIMAGING_CODEC_BROKENwhenunpacker_row_byte_size > row_byte_size(TIFFTileRowSize())._decodeAsRGBA()fails withIMAGING_CODEC_BROKENwhenstate->bits > 32, becauseTIFFRGBAImagerows are always 32 bits per pixel. This check runs beforeTIFFRGBAImageBegin()and any allocation.With the patch, the tile and RGBA files fail
load()withOSError: decoder error -2, the same result the equivalent strip file already gets. The patch also adds a regression test toTests/test_file_libtiff.py(test_unpacker_row_wider_than_libtiff_row). The test embeds the count-2 tile and the duplicated-tag YCbCr strip as base64, following the file's existing convention. The patch was applied to a clean tree and rebuilt: the reproducer no longer triggers, andTests/test_file_tiff.py,Tests/test_file_libtiff.pyandTests/test_tiff_crashes.pypass.These checks close the over-read however the two parsers are made to disagree. Separately,
_setup()could stop deriving the libtiff rawmode from tags that libtiff will read differently — e.g. treat aPhotometricInterpretation/BitsPerSample/Compressionthat is not a single well-typed value as "unknown pixel mode", the wayImageWidth/ImageLengthare already required to beintatTiffImagePlugin.py:1473, or haveImageFileDirectory_v2.load()keep the first occurrence of a repeated tag as libtiff does (a behaviour change for malformed IFDs that is better made deliberately and separately) — but that only narrows the ways to reach the C-side gap rather than removing it:TiffImagePlugincannot know how libtiff will resolve every malformed directory, and the decoder-side comparisons are what actually bound the read.The patch
Reject TIFFs whose unpacker row is wider than libtiff's decoded row in _decodeTile() and _decodeAsRGBA(), as _decodeStrip() already does
Applies to
src/libImaging/TiffDecode.cat the commit above.The reproducer
Reproducer —
repro, 4,178 bytes (click to open)Its last line is
BUGwhen the issue triggers andOKwhen it does not. Re-run on a clean build of that commit it printedBUG.Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Pillow tiled-TIFF decoder reads past tile buffer in copy4 (Unpack.c:1290)
CVE-2026-112395 / GHSA-462x-pgg6-55fp
More information
Details
Summary
Pillow's libImaging
_decodeTiledecoder, used to decode tiled TIFF images, allocates a per-tile buffer usingrealloc(state->buffer, TIFFTileSize(tiff)). After eachTIFFReadTileit then iteratestile_yfrom 0 up tocurrent_tile_lengthand calls the configured row unpacker (herecopy4) with the source pointerstate->buffer + tile_y * row_byte_size. When libtiff returns atile_bytes_sizesmaller than the worst-caserow_byte_size * tile_length, the inner stride-and-copy loop walks past the realloc'd region andcopy4performs a 1024-bytememcpythat reads up to(tile_length-1) * row_byte_size + row_byte_sizebytes from a buffer sized only totile_bytes_size. The supplied PoC TIFF triggers a 1024-byte over-read exactly 0 bytes past a 196608-byte allocation. An attacker who can convince a Pillow-using application to decode a crafted TIFF can trigger a process crash (DoS) and may cause adjacent heap memory to be copied into the decoded pixel buffer.Details
Pillow's TIFF decoder, when libtiff reports the file is tiled, takes the
_decodeTilepath. The function:TIFFTileSize(tiff)(the bytes-per-tile) andTIFFTileRowSize(tiff)(the bytes per row as stored by libtiff).state->bufferto exactlytile_bytes_size.(x, y)and each plane, callsTIFFReadTileintostate->buffer, then walks the tile row by row, calling the configuredImagingShuffler(herecopy4) on each row's worth of source bytes.The validation done before the loop is:
This rejects oversized libtiff tiles, but it does not reject undersized ones — i.e. the case where libtiff returns a
tile_bytes_sizestrictly less thanrow_byte_size * tile_length. The loop body, however, indexes into the buffer assuming the buffer is at leastrow_byte_size * tile_lengthbytes long.Vulnerable Code
The PoC's tile metadata yields
tile_width = tile_length = 256,row_byte_size = 1024(4 bytes/pixel × 256 pixels), andtile_bytes_size = 196608(which is 256·256·3 = three-channel-byte storage). The realloc'd buffer is 196608 bytes. The unpacker iteratestile_yfrom 0 to 255 and for the highest rows the source pointerstate->buffer + tile_y * 1024already lies at or past offset 196608 — the very last row'smemcpy(in=buffer+255·1024, 1024)reads 1024 bytes starting260096 - 196608 = 63488bytes past the allocation; ASAN flags the very first byte past the heap region (READ of size 1024 at … 0 bytes after 196608-byte region).The mismatch is real: libtiff's
TIFFTileSizereturns the packed per-tile storage (which depends on the file's photometric/separation layout — it can legitimately be smaller thanrow_byte_size × tile_length), while the inner loop strides as if the buffer always equalsrow_byte_size × tile_length.Attribution
This issue was found by Anthropic from using Claude to study security of open source projects, and I am from Ada Logics helping validate the found issues and report to maintainers. The data in this report has been reviewed manually.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:LReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Pillow: Unbounded recursion parsing a PDF page tree or trailer chain (incomplete fix for CVE-2026-42310)
CVE-2026-112895 / GHSA-p4wg-p73j-c528
More information
Details
Summary
When an application appends to an existing PDF with
Image.save(..., format="PDF", append=True), Pillow parses that file withPdfParser. Two recursive parsers have no effective bound.linearize_page_treefollows/Kidswith no visited set or depth limit, so a cyclic or duplicated page tree recurses without end.read_prev_trailerrejects only an exactly repeated/Prevoffset, so a long chain of unique offsets still recurses. The trailer-loop guard added for CVE-2026-42310 does not cover either case.Details
linearize_page_treerecurses on/Kidswith no cycle or depth check:src/PIL/PdfParser.py:1078-1092read_prev_trailercatches only a repeated offset:src/PIL/PdfParser.py:697-720(theprocessed_offsetscheck at:714-718is the CVE-2026-42310 fix; a unique-offset chain slips past it)PoC
Run it:
The 315-byte file triggers the recursion on parse. A duplicated (non-cyclic)
/Kidstree instead produces super-linear CPU and memory work, and a long unique-offset/Prevchain triggers the trailer-side recursion.Impact
A service that appends to user-supplied PDFs, for example a stamping or watermarking pipeline, can be driven into uncaught recursion or heavy CPU and memory use before any image is written. Follow-up to CVE-2026-42310. Since Pillow's policy discourages filing plain Python exceptions as denial of service, lead with the cyclic-tree case, which burns CPU and memory rather than only raising
RecursionError. The fix is a visited set plus a depth limit in both parsers.Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Pillow: TiffDecode returns uninitialized memory as pixels for short JPEG strips
CVE-2026-112897 / GHSA-q45j-58j2-45rg
More information
Details
Pillow hands back leftover process memory as image pixels when it opens a small crafted TIFF file. Anyone who can get an application to open their image, and can see something produced from its pixels (a thumbnail, a preview, a converted copy), can read heap memory that the process had previously used and freed. This works in the default configuration: opening and loading the 478-byte file succeeds with no error or warning, and every row below the first eight holds stale memory instead of picture content.
The bug is in
_decodeStrip()insrc/libImaging/TiffDecode.c, and_decodeTile()has the same shape. The functionrealloc()sstate->buffertoTIFFStripSize()bytes without initialising it and callsTIFFReadEncodedStrip(). Whenever that call does not return -1, it copies allRowsPerStriprows of the buffer into the image. libtiff's JPEG codec only warns when the JPEG embedded in a strip has fewer rows than the strip, and Pillow silences libtiff warnings. The codec decodes the rows that exist and still returns the full strip size, so the untouched remainder is copied out as pixels. libtiff's Group 4 fax codec behaves the same way for a stream that ends early.The attached reproducer builds a 256×256 TIFF whose single strip or tile is a 256×8 JPEG. Before loading it, the script frees buffers filled with
b"LEAK". It then finds 15,872 copies ofLEAKin rows 8–255 of the decoded image (the same freed chunk was reused in 500 of 500 runs). Under glibc'sMALLOC_PERTURB_, those rows are entirely the allocator's fill byte. A well-formed control file decodes correctly in both cases.This is a read of uninitialised heap memory, not memory corruption. Each strip can disclose up to width ×
RowsPerStripbytes, and what comes back depends on the allocator's state at the time. The strip path was introduced in e91b851fdc1c ("Add reading old-JPEG compressed TIFFs", first released in Pillow 6.0.0). The tile path has had the same pattern since f0436a4ddc95 (Pillow 5.3.0).Details
src/libImaging/TiffDecode.cdecodes compressed TIFFs through libtiff into a strip(or tile) buffer that it
realloc()s toTIFFStripSize()bytes and never initialises, andthen copies all
RowsPerStriprows of that buffer into the image wheneverTIFFReadEncodedStrip()does not return -1. libtiff's JPEG codec does not fail when theJPEG stream inside a strip has fewer rows than the strip: it decodes the rows that exist
and returns the full strip size. The rows it did not write are whatever the heap held
before, and Pillow returns them as pixels.
A 478-byte file shows it: a classic little-endian TIFF, 256×256,
BitsPerSample=8,Compression=7,PhotometricInterpretation=1,RowsPerStrip=256, one strip whose datais a baseline JPEG of 256×8 pixels (as written by
Image.new("L",(256,8),200).save(…,"JPEG")).Image.open(BytesIO(data)).load()succeeds with default settings (compressed TIFFs alwaystake the libtiff path). Rows 0–7 of the result are the JPEG content (value 200); the
remaining 63488 bytes of
im.tobytes()are heap garbage: run under glibc'sMALLOC_PERTURB_=1they are all0xFE, underMALLOC_PERTURB_=170all0x55(thefresh-
mallocfill byte), and in a process that has just freed buffers containingb"LEAK"they contain 15872 copies ofLEAK.Feeding the same file to libtiff 4.5.0 directly confirms the library side:
TIFFReadEncodedStrip(t, 0, buf, 65536)printsJPEGPreDecode: Warning, Improper JPEG strip/tile size, expected 256x256, got 256x8.,returns 65536, and leaves 63488 of the 65536 buffer bytes untouched. Pillow installs
TIFFSetWarningHandler(NULL)(TiffDecode.c:682), so the warning is not visible either.The strip path:
state->bufferarrives here as the one-rowcallocmade bydecode.c:230(
(bits*xsize+7)/8= 256 bytes for the example). Thereallocto 65536 bytes hands backa different, uninitialised chunk; nothing zeroes it.
TIFFReadEncodedStrip()returns thenominal strip size on success, not the number of rows produced, and the only check is
== -1, so the row loop unconditionally copies 256 rows × 256 bytes intoim->image(which itself is zeroed by
core.new, so the garbage provably comes fromstate->buffer)._decodeTile()has the same shape (TiffDecode.c:448realloctoTIFFTileSize(), thentile_lengthrows copied after eachTIFFReadTile()at 460–488).For a well-formed file the JPEG inside the strip is 256×256, libtiff fills all 65536 bytes
and the copy is correct. The malicious file differs only in the SOF height of the embedded
JPEG; libtiff treats "JPEG smaller than strip" as a warning, clamps the row count to the
JPEG height, and reports success.
JPEG is not the only libtiff codec that succeeds on a short strip.
Fax4Decode(
Compression=4) returns success for a T.6 stream that ends early: a 133-byte 512×2561-bit TIFF whose strip is the Group 4 stream of a 512×16 image loads without error,
libtiff writes 17 rows (1088 bytes) of the 16384-byte buffer
(
Fax4Decode: Warning, Premature EOL at line 16, return value 16384) and the other15296 bytes are again heap contents. LZW, Deflate, PackBits, Group 3 and CCITT RLE were
checked and do return -1 on short data, so Pillow raises
OSErrorfor those.The strip path was introduced in
e91b851fdc1c("Add reading old-JPEG compressed TIFFs",2018-12-08), first released in Pillow 6.0.0, which replaced row-by-row
TIFFReadScanline()(where libtiff errors on the first missing row) with the whole-strip
realloc()and copy.The tile path has had the same shape since it was added in
f0436a4ddc95(2018-07-02,Pillow 5.3.0).
Impact
Any application that opens attacker-supplied images with Pillow in the default
configuration and returns something derived from the pixels (thumbnail, preview, format
conversion, OCR text) discloses freed heap memory of the worker process to the uploader:
everything below the rows the embedded JPEG/G4 stream actually encodes, up to
ImageWidth × RowsPerStrip × SamplesPerPixelbytes per strip and tens of MB per imagewithin the default
MAX_IMAGE_PIXELS.load()raises nothing, so the application has nosignal that the image is malformed. What comes back depends on allocator state; in a
long-running web worker it is typically recently freed request bodies, other users'
decoded image buffers and Python object contents. There is no memory corruption; this is
a read of uninitialised (previously used) heap memory, analogous in effect to an
out-of-bounds read that lands in pixels.
Reproducing
reprois a short Python script that runs against the installed Pillow build withdefault settings: no rebuild, no sanitizer, no files written. It runs twice, once normally
and once re-executing itself under glibc's
MALLOC_PERTURB_=170, and the last line itprints is
BUGorOK.Input.
tiff(jpeg_rows, tiled)builds a little-endian TIFF in memory. The headerdeclares a 256×256 8-bit greyscale image with
Compression=7(JPEG), either as one strip(
RowsPerStrip=256) or as one 256×256 tile (TileWidth/TileLength). The strip or tileis a baseline JPEG of 256×
jpeg_rowspixels, all value 200, written by Pillow's own JPEGencoder. For each layout the script builds two files:
jpeg_rows=256. This is a well-formed file, and libtiff fills the whole64 KiB strip/tile.
jpeg_rows=8, 478 bytes (strip) or 490 bytes (tile). The only differenceis the embedded JPEG's height. libtiff decodes 8 rows (2048 bytes), warns
Improper JPEG strip/tile size(Pillow silences the warning) and returns the fullstrip/tile size as success.
Steps. Each file is opened (the script asserts it takes the
libtiffdecoder). Justbefore
im.load()the script allocates and frees six 64 KiBbytearrays filled withb"LEAK", which leaves freed heap chunks about the size of the buffer that_decodeStrip()/_decodeTile()is about torealloc(). For rows 8–255 it then countscopies of
LEAKand bytes equal to0x55(glibc's fresh-mallocfill underMALLOC_PERTURB_=170). The files contain noLEAKbytes (the script asserts this), soany copy in the image came from freed heap memory. It reports
BUGif a crafted fileshows any
LEAKin the normal pass, or rows 8–255 entirely0x55in the perturbed pass.Expected output on this tree:
im.load()succeeds for every file. For the crafted files rows 8–255 are the uninitialisedstrip/tile buffer, copied as-is into the image. With the attached patch applied, rows 8–255
of the crafted files come back as
\x00bytes, with noLEAKand no0x55, and thescript ends with
OK.The
LEAKcount depends on where glibc places the buffer (the same freed chunk was reusedin 500 of 500 runs here); the
MALLOC_PERTURB_pass does not. The script covers the JPEGcodec only. The Group 4 variant in the Details section (
Compression=4, a T.6 stream thatends early) goes through the same
_decodeStrip()copy, and the same fix covers it.Suggested fix
The attached patch zeroes
state->bufferbefore each libtiff read in both loops ofsrc/libImaging/TiffDecode.c— beforeTIFFReadTile()in_decodeTile()and beforeTIFFReadEncodedStrip()in_decodeStrip()— so rows a codec does not produce aredeterministic zeros rather than stale heap:
A
memsetper strip/tile (rather than onecallocafter the size is known) is neededbecause the same buffer is reused for every strip, and a short last strip would otherwise
expose the previous strip's rows — harmless for disclosure, but the per-read
memsetkeeps the behaviour uniform at the cost of one extra pass over the buffer. Promoting
libtiff warnings to errors via a warning handler is not a good alternative: both codecs
only warn (
Improper JPEG strip/tile size,Premature EOL), but so do many legitimatefiles (unknown tags, bad tag counts), and the return value of
TIFFReadEncodedStrip()carries no row count to check against. Initialising the buffer covers every codec.
_decodeAsRGBA()needs no change:TIFFRGBAImageGet()fills its whole raster, and JPEGfiles do not reach it (
JPEGCOLORMODE_RGBsends them down the strip path).Valid files decode exactly as before; rows a codec does not produce are now black instead
of garbage, and nothing that previously loaded now fails. The patch also adds
test_jpeg_short_striptoTests/test_file_libtiff.py, parametrised over strip and tile,which builds the 256×8-JPEG-in-256×256 file and asserts rows 8–255 are zero. It is a
regression guard for the fixed code rather than a reliable detector of the bug: in a fresh
pytest process the unfixed strip variant can pass because the new heap chunk happens to be
zero (the tiled variant fails before the fix). With the patch,
Tests/test_file_libtiff.pyand
Tests/test_file_tiff.pypass and the reproducer printsOK.The patch
Zero the libtiff strip/tile buffer before each TIFFReadEncodedStrip/TIFFReadTile in TiffDecode.c so short strips do not expose uninitialised heap
Applies to
src/libImaging/TiffDecode.cat the commit above.