PUBLIC MIRROR A read-only public view of Anvil. Only publicly-disclosed findings are shown; the Playbook, techniques, sessions and embargoed research are hidden.

← Findings

LIBTIFF-0002

high verified

Heap OOB write in JPEGDecodeRaw 12-bit downsampled path (unbounded tmpbuf clump write)

✓ Disclosure ready Real-world verified and packaged for disclosure (or already carries a CVE / advisory).
Crash heap-buffer-overflow (WRITE, 2 bytes/element, controlled distance)
Topmost entry point TIFFReadEncodedStrippublic API
Verified through libtiff TIFFReadEncodedStrip (public API) built against libjpeg-turbo 3.1.4.1 dual-mode, clang-20 ASan+UBSan
Real-world impact 8.1 CVSS · high
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

Targetlibtiff
ComponentJPEG codec
Locationlibtiff/tif_jpeg.c · JPEGDecodeRaw:1755-1795 (write at 1777/1789; tmpbuf alloc 1706-1709)
Entry point TIFFReadEncodedStrip public API
TIFFReadEncodedStrip → JPEGDecodeRaw
Reached 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 classoob-write
CVE
CVSS8.1 (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)
Discovered2026-07-02

Verification

Evidence ✓ Disclosure ready (real-world verified)
Verified throughlibtiff TIFFReadEncodedStrip (public API) built against libjpeg-turbo 3.1.4.1 dual-mode, clang-20 ASan+UBSan
Harness fired✅ yes
Protocol2.0
Sanitizerasan
Crash typeheap-buffer-overflow (WRITE, 2 bytes/element, controlled distance)
Reprocjpeg -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 tolibtiff maintainers (F. Warmerdam, B. Friesenhahn; cc E. Rouault) — private coordinated-disclosure email
Reported2026-07-03
Vendor ack
Embargo until
Public2026-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, span 6·ceil(W/2) = 3W+3 → writes 3 elements (6 bytes) past the heap buffer.
  • 1×2 subsampling (any width): allocation 3·W, span 4·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.