LIBHEIF-0007
medium verifiedUncompressed encoder: heap OOB write in rgb_block_pixel_interleave for RRGGBB images with bit-depth <= 8
heif_context_encode_imagepublic API libheif public C API — heif_image_create/heif_image_add_plane + heif_context_get_encoder_for_format(heif_compression_uncompressed) + heif_context_encode_image Reproduction command (asan)
ASan/UBSan libheif (clang-20), reuse project/builds/unc_asan; see pocs/libheif/LIBHEIF-0007/verify.sh
./harness_enc # encodes a 64x64 RGB interleaved_RRGGBB_LE @ 8-bit image
# expect: heap-buffer-overflow WRITE in unc_encoder_rgb_block_pixel_interleave::encode_tile (line 116), 0 bytes after a W*H*3 region Classification
| Target | libheif |
|---|---|
| Component | Uncompressed codec |
| Location | libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.cc · unc_encoder_rgb_block_pixel_interleave::encode_tile:112-119 |
| Entry point | heif_context_encode_image public API heif_context_encode_image (heif_encoding.cc:711) → HeifContext::encode_image (context.cc:1614) → ImageItem::encode_to_item -> encode_to_bitstream_and_boxes (image_item.cc:388/251) → ImageItem_uncompressed::encode (unc_image.cc:100) → unc_encoder::encode (unc_encoder.cc:251) -> unc_encoder_rgb_block_pixel_interleave::encode_tile (unc_encoder_rgb_block_pixel_interleave.cc:116)Reproduced through the TOPMOST public encode API heif_context_encode_image (2026- 06-23) — not just the internal factory. Still encoder-side / NOT file-triggerable: a caller must build a HeifPixelImage with colorspace RGB + chroma interleaved_RRGGBB_{LE,BE} at interleaved bit-depth <=8 and encode to the uncompressed codec (heif_compression_uncompressed). The decoder never produces that config (8-bit RGB decodes to interleaved_RGB), so it is NOT reachable via a decode->encode transcode; exposure is apps that construct such images from caller-controlled data. Public-API reachable -> evidence public_api_reachable. |
| Vuln class | oob-write |
| CVE | — |
| CVSS | 5.3 (CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L) |
| Discovered | 2026-06-21 |
Verification
| Evidence | ✓ Disclosure ready (real-world verified) |
|---|---|
| Verified through | libheif public C API — heif_image_create/heif_image_add_plane + heif_context_get_encoder_for_format(heif_compression_uncompressed) + heif_context_encode_image |
| Harness fired | ✅ yes |
| Protocol | 2.0 |
| Sanitizer | asan |
| Crash type | heap-buffer-overflow (WRITE, size 1) |
| Repro | Built libheif @ 78638f4f with -fsanitize=address,undefined (clang-20). Harness builds a HeifPixelImage(64x64, colorspace RGB, chroma interleaved_RRGGBB_LE, bit-depth 8) and calls unc_encoder_factory::get_unc_encoder()+encode(). The block-pixel encoder is selected; it computes m_bytes_per_pixel = (3*bpp+7)/8 = 3 and sizes the output buffer W*H*3 = 12288 bytes, but its inner loop writes 4 bytes per pixel (W*H*4 = 16384). ASan reports a heap-buffer-overflow WRITE 0 bytes after the 12288-byte region, in encode_tile:116. RE-VERIFIED 2026-06-22 on a freshly- rebuilt pristine 78638f4f ASan lib: same heap-buffer-overflow WRITE at unc_encoder_rgb_block_pixel_interleave.cc:116. PUBLIC-API repro added 2026-06-23 (poc_public_encode.c): heif_image_create + heif_image_add_plane(...,8) + heif_context_get_encoder_for_format(heif_compression_uncompressed) + heif_context_encode_image -> the same OOB write, full stack heif_context_encode_image (heif_encoding.cc:711) -> HeifContext::encode_image -> ImageItem_uncompressed::encode (unc_image.cc:100) -> unc_encoder::encode -> encode_tile:116. -> evidence public_api_reachable. Also empirically reproduced on the v1.22.0 RELEASE (harness_enc_v1220.cc) -> still LIVE there. |
Reproduce / self-verify
# build
ASan/UBSan libheif (clang-20), reuse project/builds/unc_asan; see pocs/libheif/LIBHEIF-0007/verify.sh
# run
./harness_enc # encodes a 64x64 RGB interleaved_RRGGBB_LE @ 8-bit image
# expect (asan):
heap-buffer-overflow WRITE in unc_encoder_rgb_block_pixel_interleave::encode_tile (line 116), 0 bytes after a W*H*3 region cost: low (64x64 image, no large alloc)
Disclosure
| Reported to | GitHub security advisory GHSA-46rp-pcq2-rpmr — strukturag/libheif |
|---|---|
| Reported | 2026-06-23 |
| Vendor ack | — |
| Embargo until | — |
| Public | — |
| Patched in | — |
PoC: pocs/libheif/LIBHEIF-0007/poc_public_encode.c (PUBLIC C API: heif_context_encode_image; asan-public-encode-api.txt) + harness_enc.cc / harness_enc_v1220.cc (internal-factory variants) + verify.sh, asan-encoder-rrggbb8.txt, asan-encoder-rrggbb8-v1.22.0.txt
Writeup
Affected range & upstream status (2026-06-23) — LIVE in v1.22.0
Release-tag bisect (see pocs/libheif/disclosure/bisect/BISECT-RESULTS.md):
- Affected: v1.22.0 only (the latest release) + pre-release master (e.g.
78638f4f). The buggy fileunc_encoder_rgb_block_pixel_interleave.ccwas introduced by the v1.22.0 encoder rewrite; the older monolithicplugins/encoder_uncompressed.cc(≤ v1.21.2) has no RRGGBB / block-pixel encode path, so releases ≤ v1.21.2 are not affected. - NOT fixed — empirically confirmed LIVE in v1.22.0. A harness ported to the
v1.22.0
HeifPixelImageAPI (add_plane→add_channel,pixelimage.h→image/pixelimage.h;harness_enc_v1220.cc) built against a clean v1.22.0 tree crashes under ASan:heap-buffer-overflow WRITE0 bytes after the 12288-byte (64×64×3) buffer atunc_encoder_rgb_block_pixel_interleave.cc:116, viaunc_encoder::encode(unc_encoder.cc:241) →encode_tile— identical to the 78638f4f reproduction (log:pocs/libheif/LIBHEIF-0007/asan-encoder-rrggbb8-v1.22.0.txt). This is the one finding of LIBHEIF-0006/7/8/9/10 still live in the current release. Reported upstream 2026-06-23 as GitHub security advisory GHSA-46rp-pcq2-rpmr (verified patch + public-API PoC included). Awaiting maintainer triage / fix / CVE. - This makes 0007 the genuine disclosure candidate (the others are all fixed in
v1.22.0). Reachability remains encoder-side / low (see Impact) — an application
must build an
interleaved_RRGGBB_{LE,BE}image at bit-depth ≤8 and encode it to the uncompressed codec — so severity stays medium, but it is a real OOB write in the shipping release. TheLIBHEIF-0007-encoder-rrggbb.patchstill applies.
Summary
The uncompressed encoder path unc_encoder_rgb_block_pixel_interleave writes
past its output buffer when encoding an RGB interleaved_RRGGBB_{LE,BE} image
whose interleaved bit-depth makes m_bytes_per_pixel < 4. The buffer is sized
W*H*m_bytes_per_pixel, but the per-pixel write loop always emits 4 bytes
(5 when m_bytes_per_pixel > 4), so for m_bytes_per_pixel ∈ {1,2,3} it writes
4 bytes into a ≤3-byte/pixel buffer — a heap out-of-bounds write of (4 - m_bytes_per_pixel)*W*H bytes of attacker pixel data.
m_bytes_per_pixel = (3*bpp + 7) / 8, and can_encode accepts bpp < 14, so:
| bpp | m_bytes_per_pixel | bytes written/px | result |
|---|---|---|---|
| 1–8 | 1–3 | 4 | OOB write |
| 9–10 | 4 | 4 | ok |
| 11–13 | 5 | 5 | ok |
Reproduction
See pocs/libheif/LIBHEIF-0007/. harness_enc.cc builds a 64×64 RGB
interleaved_RRGGBB_LE image at bit-depth 8 and runs the uncompressed encoder:
ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 1
#0 unc_encoder_rgb_block_pixel_interleave::encode_tile unc_encoder_rgb_block_pixel_interleave.cc:116
#1 unc_encoder::encode unc_encoder.cc:251
0x... is located 0 bytes after 12288-byte region (12288 = 64*64*3)
Root cause
uint16_t nBits = 3 * bpp;
m_bytes_per_pixel = static_cast<uint8_t>((nBits + 7) / 8); // 3 at bpp=8
...
out_size = W * H * m_bytes_per_pixel; // 3 bytes/pixel
data.resize(out_size);
...
for each pixel:
if (m_bytes_per_pixel > 4) { *p++ = >>32; } // only the 5-byte case is guarded
*p++ = >>24; *p++ = >>16; *p++ = >>8; *p++ = &0xFF; // always 4 bytes
The loop hard-codes a 4-byte tail and only conditionally prepends a 5th byte; it
never handles the 1/2/3-byte cases that m_bytes_per_pixel can take, so the
written length exceeds the allocation whenever m_bytes_per_pixel < 4. The sister
encoders size and write consistently; only this block-pixel path is wrong.
Impact
Heap out-of-bounds write (CWE-787) of attacker pixel bytes during encoding.
Unlike [[LIBHEIF-0006]] (a decode-path bug reachable from any crafted file), this
is encoder-side: it requires the application to encode a HeifPixelImage that
is colorspace RGB, chroma interleaved_RRGGBB_{LE,BE}, with interleaved bit-depth
≤ 8 (bpp 9–13 are sized correctly and are benign). The
normal decoder does not produce RRGGBB at ≤8-bit (8-bit RGB decodes to
interleaved_RGB), so it is not reachable through a simple decode→encode
transcode; practical exposure is limited to applications that build such images
from caller-controlled data and encode them to the uncompressed codec. Severity
medium (true memory-corruption primitive, but local / non-file-triggerable and
an atypical input configuration).
Suggested fix
Write exactly m_bytes_per_pixel bytes per pixel (emit the low
m_bytes_per_pixel bytes of combined_pixel in big-endian order via a length-
generic loop), or reject m_bytes_per_pixel < 4 in can_encode. Add an encode
round-trip test covering RRGGBB at bit-depths 8–13.
Patch
Verified fix: pocs/libheif/disclosure/patches/LIBHEIF-0007-encoder-rrggbb.patch (applied to 78638f4f, rebuilt with ASan/UBSan, PoC re-run confirms the sanitizer no longer fires and valid inputs still work). Consolidated write-up: pocs/libheif/disclosure/ADVISORY.md.