LIBTIFF-0002
high verifiedHeap OOB write in JPEGDecodeRaw 12-bit downsampled path (unbounded tmpbuf clump write)
TIFFReadEncodedStrippublic API libtiff TIFFReadEncodedStrip (public API) built against libjpeg-turbo 3.1.4.1 dual-mode, clang-20 ASan+UBSan Reproduction command (asan)
Build libjpeg-turbo>=3.0 (dual mode) + libtiff -Djpeg=ON against it, both under clang-20 -fsanitize=address,undefined; see pocs/libtiff/verify.sh LIBTIFF-0002 (uses ~/Projects/libjpeg-turbo/matrix_runs/src-3.1.4.1)
decode_harness_j12 pocs/libtiff/LIBTIFF-0002/poc.tif (calls TIFFReadEncodedStrip) — or pocs/libtiff/verify.sh LIBTIFF-0002
# expect: AddressSanitizer: heap-buffer-overflow WRITE of size 2 in JPEGDecodeRaw at tif_jpeg.c:1789 (tmpbuf allocated at tif_jpeg.c:1706) Classification
| Target | libtiff |
|---|---|
| Component | JPEG codec |
| Location | libtiff/tif_jpeg.c · JPEGDecodeRaw:1755-1795 (write at 1777/1789; tmpbuf alloc 1706-1709) |
| Entry point | TIFFReadEncodedStrip public API TIFFReadEncodedStrip → JPEGDecodeRawReached from the public TIFFReadEncodedStrip / TIFFReadEncodedTile with the DEFAULT JPEGCOLORMODE (RAW) — i.e. reading downsampled YCbCr strips directly (the common GDAL-style consumer pattern). Requires libtiff built with libjpeg-turbo >= 3.0 dual mode (HAVE_JPEGTURBO_DUAL_MODE_8_12), which is the default on Ubuntu 24.04+, Debian 13, Fedora, RHEL 9. tiffcp/tiff2rgba set JPEGCOLORMODE_RGB (→ the non-raw JPEGDecode path) so they are NOT affected; consumers that read raw YCbCr are. |
| Vuln class | oob-write |
| CVE | — |
| CVSS | 8.1 (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H) |
| Discovered | 2026-07-02 |
Verification
| Evidence | ✓ Disclosure ready (real-world verified) |
|---|---|
| Verified through | libtiff TIFFReadEncodedStrip (public API) built against libjpeg-turbo 3.1.4.1 dual-mode, clang-20 ASan+UBSan |
| Harness fired | ✅ yes |
| Protocol | 2.0 |
| Sanitizer | asan |
| Crash type | heap-buffer-overflow (WRITE, 2 bytes/element, controlled distance) |
| Repro | cjpeg -precision 12 -sample 2x2 on a 17x16 RGB PPM -> 12-bit YCbCr 2x2 JPEG; embed as the single strip of a COMPRESSION_JPEG TIFF (w=17,l=16,spp=3,bps=12,YCbCrSub=2,2); TIFFReadEncodedStrip decodes via JPEGDecodeRaw -> ASan heap-buffer-overflow at tif_jpeg.c:1789. Also reproduced by a standalone extraction harness (pocs/libtiff/LIBTIFF-0002/harness.c) for both 2x2-odd and 1x2 geometries. |
Reproduce / self-verify
# build
Build libjpeg-turbo>=3.0 (dual mode) + libtiff -Djpeg=ON against it, both under clang-20 -fsanitize=address,undefined; see pocs/libtiff/verify.sh LIBTIFF-0002 (uses ~/Projects/libjpeg-turbo/matrix_runs/src-3.1.4.1)
# run
decode_harness_j12 pocs/libtiff/LIBTIFF-0002/poc.tif (calls TIFFReadEncodedStrip) — or pocs/libtiff/verify.sh LIBTIFF-0002
# expect (asan):
AddressSanitizer: heap-buffer-overflow WRITE of size 2 in JPEGDecodeRaw at tif_jpeg.c:1789 (tmpbuf allocated at tif_jpeg.c:1706) cost: medium (must build libjpeg-turbo 3.x dual mode; tiny input, no large alloc)
Disclosure
| Reported to | libtiff maintainers (F. Warmerdam, B. Friesenhahn; cc E. Rouault) — private coordinated-disclosure email |
|---|---|
| Reported | 2026-07-03 |
| Vendor ack | — |
| Embargo until | — |
| Public | 2026-07-04 |
| Patched in | — |
PoC: pocs/libtiff/LIBTIFF-0002/poc.tif
Writeup
Summary
JPEGDecodeRaw() decodes downsampled (subsampled YCbCr) JPEG strips/tiles clump
by clump. For the 8-bit build it writes directly into the caller buffer buf and
bounds-checks every clump against cc (tif_jpeg.c:1759-1769 — the guard added for
CVE-2020-19131). For the 12-bit / MK1 build (JPEG_LIB_MK1_OR_12BIT, active
when libtiff is built against libjpeg-turbo ≥ 3.0 dual mode) it instead writes into
a scratch tmpbuf (tif_jpeg.c:1756) with no bounds check at all.
tmpbuf is allocated as output_width * num_components unsigned-short elements
(tif_jpeg.c:1706-1709), but the clump loop writes across
samples_per_clump * clumps_per_line element positions:
samples_per_clump = Σ(h_i·v_i)over components;clumps_per_line = comp_info[1].downsampled_width = ceil(output_width / max_h).- 2×2 subsampling, odd width W: allocation
3·W, span6·ceil(W/2) = 3W+3→ writes 3 elements (6 bytes) past the heap buffer. - 1×2 subsampling (any width): allocation
3·W, span4·W→ ~W-element OOB.
The overwritten values are the attacker-influenced decoded 12-bit samples.
Reproduction
pocs/libtiff/LIBTIFF-0002/poc.tif — a well-formed 17×16 (odd width), YCbCr,
bps=12, YCbCrSubSampling=2,2 COMPRESSION_JPEG TIFF (single strip = a 12-bit 2×2
JPEG produced by cjpeg -precision 12 -sample 2x2). No malformed tags.
decode_harness_j12 poc.tif # calls TIFFReadEncodedStrip (default JPEGCOLORMODE)
==ERROR: AddressSanitizer: heap-buffer-overflow WRITE of size 2 ...
#0 JPEGDecodeRaw libtiff/tif_jpeg.c:1789
#1 TIFFReadEncodedStrip libtiff/tif_read.c:608
allocated by:
#1 JPEGDecodeRaw libtiff/tif_jpeg.c:1706
SUMMARY: AddressSanitizer: heap-buffer-overflow tif_jpeg.c:1789 in JPEGDecodeRaw
pocs/libtiff/verify.sh LIBTIFF-0002 builds the toolchain and reproduces
end-to-end. pocs/libtiff/LIBTIFF-0002/harness.c is a standalone faithful
extraction of the 12-bit clump loop (./h 17 2 2, ./h 16 1 2) proving the same
OOB from the exact arithmetic without a 12-bit libjpeg.
Root cause
The 12-bit tmpbuf sizing was copied from the non-downsampled scanline scratch
(output_width*num_components is correct only when writing one packed scanline).
In the downsampled path the correct size is samples_per_clump*clumps_per_line
(what TIFFScanlineSize64 computes from samplingblocks_hor*samplingblock_samples),
and the 8-bit branch’s per-clump bounds guard is missing from the 12-bit branch.
Verification evidence
Reproduced end-to-end through the public TIFFReadEncodedStrip API of a libtiff
4.7.2 built against libjpeg-turbo 3.1.4.1 (dual 8/12-bit mode) under clang-20
ASan+UBSan: heap-buffer-overflow WRITE at tif_jpeg.c:1789, buffer allocated at
tif_jpeg.c:1706. The trigger is a well-formed odd-width (or 1×2-subsampled)
12-bit YCbCr JPEG — no tag/stream mismatch required. Independently corroborated by
the extraction harness for both geometries.
Impact
Apply impact-rubric: severity high. Attacker-controlled heap out-of-bounds
write (decoded 12-bit sample values) reached from the public strip/tile decode
API on a crafted — or even legitimately-encoded odd-width — TIFF, with no special
caller configuration (default RAW JPEGCOLORMODE). Affects any application linking a
libtiff built with libjpeg-turbo ≥ 3.0 dual mode (now the distro default) that
reads subsampled-YCbCr JPEG-in-TIFF strips/tiles directly — e.g. GDAL and other
geospatial/imaging pipelines. The 1×2 case gives a larger (~image-width) controlled
overwrite. Not reachable through RGBA/JPEGCOLORMODE_RGB consumers (tiffcp,
tiff2rgba). Bounded distance (6 bytes for 2×2-odd; ~2·width bytes for 1×2), heap
adjacency-dependent for exploitability.