Repository navigation
chore(dependencies): update dependency pillow to v13 [security] - #265
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
Contributor
Author
|
renovate
Bot
force-pushed
the
renovate/pypi-pillow-vulnerability
branch
from
October 22, 2026 12:38
1eb0cdd to
954edb4
Compare
renovate
Bot
force-pushed
the
renovate/pypi-pillow-vulnerability
branch
from
October 22, 2026 21:20
954edb4 to
0fdeb68
Compare
|
This branch has not been deployed
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.3.0→==13.0.0Pillow: Heap buffer overflow in ImagingJpegDecode
CVE-2026-112403 / GHSA-8g5g-8vmr-9782
More information
Details
Summary
JpegImageFile.draft()— whichImage.thumbnail()calls automatically — requests 1/2, 1/4 or 1/8 scaled JPEG output and shrinksself._sizeand the tile extents to match, so_setimage()allocates a single-row decode buffer ofceil(W/s)samples.ImagingJpegDecode()setscinfo.scale_num/scale_denom, callsjpeg_start_decompress(), and then reads scanlines into that buffer without ever comparingcinfo.output_width,cinfo.output_heightorcinfo.out_color_componentsagainststate->xsize,state->ysizeandstate->bits.libjpeg-turbo 3.x decodes lossless (SOF3) JPEG, but hardwires lossless streams to no scaling — the requested
scale_denomis silently discarded. It therefore returns full-widthW-sample rows into a buffer sized forceil(W/s). Eachjpeg_read_scanlines()call writes(W - ceil(W/s)) x componentsbytes past the end of the heap allocation, and the overflowing bytes are the decoded pixel values, which are byte-exact for an 8-bit grayscale lossless image.The input is a standards-compliant JPEG, so
Image.open(..., formats=["JPEG"])does not prevent it. Pillow's Python SOF parser already accepts marker0xFFC3("SOF3", "Spatial lossless") at 8-bit precision, so the file opens normally as aJPEG/ modeLimage at its full declared size.Affected code
At
e41083f383c9cd3db95de52564cc0b6452313d4a(main, 2026-08-29):src/libImaging/JpegDecode.c:224-262—ImagingJpegDecode();jpeg_start_decompress()at :240,jpeg_read_scanlines()at :252, with no geometry check between them.src/decode.c:222-231—_setimage();state->buffer = calloc(1, state->bytes)withstate->bytes = (state->bits * state->xsize + 7) / 8.src/PIL/JpegImagePlugin.py:428-464—JpegImageFile.draft(); computessand shrinks_size, the tile extents anddecoderconfigwith no notion of lossless coding.src/PIL/JpegImagePlugin.py:271—0xFFC3: ("SOF3", "Spatial lossless", SOF).Because
state->ysize(ceil(H/s)) can never equal libjpeg'soutput_heightwhens >= 2, the decode always ends in an error after the rows have already been written: the caller sees an exception and gets no pixels back. TheOSErroris a consequence of the overflow, not the issue being reported.Primitive
Per scanline: a contiguous write of
(W - ceil(W/s)) x cbytes immediately past acalloc(ceil(W/s) x c)block, wherec = 1for modeLand 4 for RGB (RGBX) and CMYK; repeated forceil(H/s)rows over the same region. The buffer size class is chosen by the attacker throughW, and for grayscale the overflowing content is an arbitrary attacker-chosen byte string.Measured in the reproducer below: 1200x1100 mode
L,s=2— a 600-byte allocation, 600 bytes written past it, ASan reporting the whole 1200-byte row write as onememcpy.Threshold:
draft()must selects >= 2, i.e.W // requested_w >= 2andH // requested_h >= 2. Forthumbnail((256,256))with the defaultreducing_gap=2.0that meansW, H >= 1024.Reproduction
Self-contained Docker reproducer (Ubuntu 24.04, clang 20, libjpeg-turbo 3.1.4 and Pillow both built from source with
-fsanitize=address; Pillow checked out at${TARGET_COMMIT}, defaulte41083f383c9cd3db95de52564cc0b6452313d4a). Save the three files below asDockerfile,make_sof3.pyandtrigger.pyin an empty directory, then:make_sof3.pyis a self-contained encoder that emits a standards-compliant 8-bit grayscale lossless-Huffman JPEG (SOI / DHT / SOF3P=8, Y=1100, X=1200, Nf=1/ SOSSs=1predictor 1,Se=0,Ah=Al=0/ entropy-coded row differences / EOI), whose rows carry a repeating marker pattern.trigger.pyopens it and callsthumbnail(), whose implicitdraft()pickss=2and allocates a 600-byte row buffer.Dockerfilemake_sof3.pytrigger.pyValidated output:
Negative control, same image: a baseline DCT JPEG of identical dimensions thumbnails cleanly, because libjpeg honours 1/2 scaling exactly as
draft()computed it.Scope and caveats
mainate41083f383c9cd3db95de52564cc0b6452313d4a, which is currentorigin/mainas of 2026-08-30. Also reported against 12.3.0.Image.thumbnail()callsdraft()by default, and the triggering bytes are entirely file data.draft()before loading are affected.open().load()andresize()decode at scale 1 and are unaffected. Callingimg.load()beforethumbnail(), orthumbnail(..., reducing_gap=None), avoids the path in an application today.JERR_ARITH_NOTIMPLbefore any row; 12- and 16-bit lossless are rejected by Pillow'sSOF()because it requiresbits == 8; SOF5-7/13-15 fail withJERR_SOF_UNSUPPORTED.Credit and disclosure
Found by Anthropic using Claude to study the security of open-source software, validated manually by Ada Logics.
This report follows Anthropic's coordinated vulnerability disclosure policy
(https://www.anthropic.com/coordinated-vulnerability-disclosure), with a 90-day
disclosure window from the date of this report.
Credit goes to Claude (Anthropic) and Ada Logics.
Severity
CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Pillow: 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: TIFF encoder ColorMap plane-size mismatch causes a heap out-of-bounds read
CVE-2026-112896 / GHSA-pchr-8j38-3x4p
More information
Details
Summary
A small attacker-supplied TIFF can be opened and loaded normally, then reach an out-of-bounds read when an application saves it back to TIFF with retained metadata. The fixed reproducer is a 1×1, 16-bit grayscale, PackBits-compressed TIFF containing tag 320 (ColorMap) with 768 SHORT values: three zero-filled planes of 256 entries each. No Python object is forged and no metadata is injected after loading.
Pillow's TIFF encoder allocates 768 × 2 = 1536 bytes, then passes three plane pointers separated by 256 SHORT entries to libtiff. With
BitsPerSample = 16, libtiff instead expects 65536 SHORT values per plane and attempts a 131072-byte read from a plane pointer. Existing ASAN observations identify a heap-buffer-overflow READ crossing the 1536-byte allocation made inPyImaging_LibTiffEncoderNew.The malformed ColorMap need not be a valid palette for this grayscale image. The issue is that accepted metadata is forwarded on save without reconciling the fixed plane extent with the effective sample depth. Opening/loading alone is not the demonstrated fault; the encoder path is required.
Impact and prerequisites
An application must accept the input, retain the relevant TIFF metadata, and invoke the libtiff-backed TIFF save path. The observed consequence is an out-of-bounds read and ASAN termination. Process-memory exposure during encoding is a potential consequence, not demonstrated by extracting arbitrary heap contents in this report. No arbitrary read primitive, full memory-disclosure chain, code execution, address-space bypass, or cross-process effect is claimed.
The 131072-byte attempted copy is not the number of bytes proven disclosed. This report does not run a synthetic-canary serializer or collect an unsanitized encoded output.
Tested environment and limits of the evidence
The existing observations were recorded on August 30, 2026, with CPython 3.11.8 on macOS ARM64, using the unmodified official Pillow 12.3.0 wheel and its bundled libtiff 4.7.1:
pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl4785266bytes37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6bb1d8e8ab8d29048624d96e3ee53cecf7c13d13de8988717b7470394a47d5ccd343130796b24ee70b6d885d3818e0e0b53238be1Apple's ASAN runtime was injected at process startup. This is **not an ASAN-rebui