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

LIBHEIF-0007

medium verified

Uncompressed encoder: heap OOB write in rgb_block_pixel_interleave for RRGGBB images with bit-depth <= 8

✓ Disclosure ready Real-world verified and packaged for disclosure (or already carries a CVE / advisory).
Crash heap-buffer-overflow (WRITE, size 1)
Topmost entry point heif_context_encode_imagepublic API
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
Real-world impact 5.3 CVSS · medium
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

Targetlibheif
ComponentUncompressed codec
Locationlibheif/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 classoob-write
CVE
CVSS5.3 (CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L)
Discovered2026-06-21

Verification

Evidence ✓ Disclosure ready (real-world verified)
Verified throughlibheif 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
Protocol2.0
Sanitizerasan
Crash typeheap-buffer-overflow (WRITE, size 1)
ReproBuilt 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 toGitHub security advisory GHSA-46rp-pcq2-rpmr — strukturag/libheif
Reported2026-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 file unc_encoder_rgb_block_pixel_interleave.cc was introduced by the v1.22.0 encoder rewrite; the older monolithic plugins/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 HeifPixelImage API (add_planeadd_channel, pixelimage.himage/pixelimage.h; harness_enc_v1220.cc) built against a clean v1.22.0 tree crashes under ASan: heap-buffer-overflow WRITE 0 bytes after the 12288-byte (64×64×3) buffer at unc_encoder_rgb_block_pixel_interleave.cc:116, via unc_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. The LIBHEIF-0007-encoder-rrggbb.patch still 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:

bppm_bytes_per_pixelbytes written/pxresult
1–81–34OOB write
9–1044ok
11–1355ok

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.

References