LIBTIFF-0003
medium verifiedHeap OOB read (+ misaligned load) in JPEGEncodeRaw 12-bit path — packed samples read as 2-byte
TIFFWriteEncodedStrippublic API libtiff TIFFWriteEncodedStrip (public API) built against libjpeg-turbo 3.1.4.1 dual-mode, clang-20 ASan+UBSan Reproduction command (asan)
libjpeg-turbo>=3.0 dual mode + libtiff -Djpeg=ON under clang-20 ASan+UBSan (same toolchain as verify.sh LIBTIFF-0002); compile pocs/libtiff/LIBTIFF-0003/encode_poc.c against build-jpeg12 libtiff + turbo3 libjpeg
./enc 16 /tmp/out.tif (even width -> heap OOB read) ; ./enc 17 /tmp/out.tif (odd -> misaligned load UB)
# expect: AddressSanitizer: heap-buffer-overflow READ of size 2 in JPEGEncodeRaw at tif_jpeg.c:2496 (buffer sized TIFFStripSize); on odd width, UBSan misaligned load of J12SAMPLE at tif_jpeg.c:2496 Classification
| Target | libtiff |
|---|---|
| Component | JPEG codec |
| Location | libtiff/tif_jpeg.c · JPEGEncodeRaw:2486/2496 (read); bytesperclumpline 2447-2453 |
| Entry point | TIFFWriteEncodedStrip public API TIFFWriteEncodedStrip → JPEGEncodeRawReached from the public TIFFWriteEncodedStrip / TIFFWriteScanline when writing a 12-bit subsampled YCbCr JPEG with the DEFAULT (RAW) JPEGColorMode — i.e. an app supplying raw pre-subsampled sample data (attacker-influenced when transcoding untrusted 12-bit images that way). Requires libtiff built vs libjpeg-turbo >= 3.0 dual mode (HAVE_JPEGTURBO_DUAL_MODE_8_12), the modern distro default. tiffcp/tiff2rgba set JPEGCOLORMODE_RGB (→ non-raw JPEGEncode) and are unaffected. |
| Vuln class | oob-read |
| CVE | — |
| CVSS | 6.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:L) |
| Discovered | 2026-07-02 |
Verification
| Evidence | ✓ Disclosure ready (real-world verified) |
|---|---|
| Verified through | libtiff TIFFWriteEncodedStrip (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 (READ, 2 bytes) + misaligned load (odd width) |
| Repro | TIFFOpen w; set width(=16 even or 17 odd) length=16 bps=12 spp=3 photometric=YCbCr compression=JPEG subsampling=2,2 rowsperstrip=16; buf=calloc(TIFFStripSize); TIFFWriteEncodedStrip(0,buf,TIFFStripSize) -> ASan heap-buffer-overflow READ at tif_jpeg.c:2496 (all tested widths 8/16/24/32/48). Odd widths additionally trip UBSan misaligned load. |
Reproduce / self-verify
# build
libjpeg-turbo>=3.0 dual mode + libtiff -Djpeg=ON under clang-20 ASan+UBSan (same toolchain as verify.sh LIBTIFF-0002); compile pocs/libtiff/LIBTIFF-0003/encode_poc.c against build-jpeg12 libtiff + turbo3 libjpeg
# run
./enc 16 /tmp/out.tif (even width -> heap OOB read) ; ./enc 17 /tmp/out.tif (odd -> misaligned load UB)
# expect (asan):
AddressSanitizer: heap-buffer-overflow READ of size 2 in JPEGEncodeRaw at tif_jpeg.c:2496 (buffer sized TIFFStripSize); on odd width, UBSan misaligned load of J12SAMPLE at tif_jpeg.c:2496 cost: medium (build libjpeg-turbo 3.x dual mode; tiny input)
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-0003/encode_poc.c
Writeup
Summary
JPEGEncodeRaw() packs the caller’s raw subsampled samples into libjpeg’s
downsampled buffers. It reads the input as
inptr = (TIFF_JSAMPLE *)buf + clumpoffset (tif_jpeg.c:2479) and consumes
samples_per_clump * clumps_per_line samples per clumpline (reads at 2486 / 2496,
inptr += samples_per_clump). In the 12-bit build TIFF_JSAMPLE is a 2-byte
J12SAMPLE, so it reads 2 * samples_per_clump * clumps_per_line bytes per
clumpline.
But 12-bit TIFF sample data is stored bit-packed (12 bits/sample, 1.5 bytes) —
the decode side packs it (tif_jpeg.c:1811-1828) — and the per-clumpline stride
bytesperclumpline (tif_jpeg.c:2447-2453) is correctly computed as
ceil(clumps * (h*v+2) * data_precision / 8) ≈ 1.5×, matching TIFFStripSize.
The encoder therefore reads 2 bytes/sample from a 1.5-byte/sample buffer and runs
~0.5 * samples_per_clumpline bytes past the end of the TIFFStripSize-sized input
strip on the final clumpline:
- even width → heap-buffer-overflow READ (tif_jpeg.c:2496)
- odd width →
buf += bytesperclumplineadvances an odd number of bytes, so(J12SAMPLE*)bufis misaligned → UBSan misaligned-load UB
The over-read bytes are entropy-coded into the output JPEG, i.e. adjacent heap memory is disclosed into attacker-observable output.
Reproduction
pocs/libtiff/LIBTIFF-0003/encode_poc.c writes a 12-bit YCbCr 2×2 JPEG via
TIFFWriteEncodedStrip, buffer sized to TIFFStripSize() (canonical usage),
default RAW JPEGColorMode.
./enc 16 /tmp/out.tif
==ERROR: AddressSanitizer: heap-buffer-overflow READ of size 2
#0 JPEGEncodeRaw libtiff/tif_jpeg.c:2496
#1 TIFFWriteEncodedStrip libtiff/tif_write.c:342
allocated by: calloc(TIFFStripSize=576) in main
SUMMARY: heap-buffer-overflow tif_jpeg.c:2496 in JPEGEncodeRaw
Fires for every tested width (8/16/24/32/48). See repro.log.
Root cause
The 12-bit raw encode path treats a bit-packed 12-bit input buffer as an array of
2-byte J12SAMPLE. Either the input must be unpacked to 2-byte samples (and
bytesperclumpline/TIFFStripSize sized to match), or the reads must honor the
packed layout. As written, TIFFStripSize (1.5×, packed) disagrees with the
encoder’s 2× read, so a caller trusting TIFFStripSize over-reads. This is the
encode-side sibling of LIBTIFF-0002 (the 12-bit downsampled sizing mismatch).
Verification evidence
Reproduced end-to-end through the public TIFFWriteEncodedStrip API of 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 READ at tif_jpeg.c:2496 on a TIFFStripSize-sized
buffer, plus misaligned-load UB for odd widths.
Impact
Apply impact-rubric: severity medium. Heap out-of-bounds read reachable from
the public strip/tile write API when encoding 12-bit subsampled YCbCr JPEG in raw
mode, with canonical buffer sizing (TIFFStripSize). Two consequences: (1)
information disclosure — adjacent heap bytes are encoded into the output JPEG,
observable by whoever receives the file; (2) potential crash on an unmapped page.
Narrower than the decode-side write bug (LIBTIFF-0002): the raw-encode path requires
an application that supplies pre-subsampled 12-bit data without
JPEGCOLORMODE_RGB, and a 12-bit (turbo ≥3.0 dual-mode) libtiff. No OOB write and
no integrity impact. The 12-bit raw-encode path is also functionally broken
(mis-packs), so it is likely rarely exercised in the wild.