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-0003

medium confirmed

Fraction(uint32_t,uint32_t) assert / silent int32 narrowing on clap geometry

Static only Source / code-trace analysis only. Nothing has been executed.

⚠ Harness reproduced — not real-world verified. Reproduce through the public API, a real application, or a platform decoder before treating this as verified.

Crash reachable assertion/SIGABRT (debug) or silent int32 narrowing (release) via the tiling API — per upstream advisory GHSA-jc8f-p23p-5hjg; not reproduced in-house (our decode-path PoC is size-limited)
Topmost entry point heif_context_read_from_file / heif_decode_imagepublic API
Verified through no real consumer named
Real-world impact medium
Reproduction command (none)
cmake ASan/UBSan Debug libheif @ 78638f4f, clang-20 (asserts ON; reuse project/builds/unc_asan)
./decode_file F3-fuzz-423b.bin   # public API heif_context_read_from_file -> heif_decode_image
# expect: (historic claim) box.cc:85 Assertion 'num <= INT32_MAX' failed — NOT reproduced on 78638f4f (see verdict)

Classification

Targetlibheif
ComponentISO-BMFF box parser
Locationlibheif/box.cc · Fraction::Fraction(uint32_t, uint32_t):83-89
Entry point heif_context_read_from_file / heif_decode_image public API
heif_decode_image → (clap transform) Box_clap::left_rounded/right_rounded/top_rounded/bottom_rounded (box.cc:3636/3651..) → Fraction(uint32_t,uint32_t) (box.cc:83-89) assert // NOT reached: intercepted by image-size security limit
The asserting Fraction(uint32_t,uint32_t) ctor is NOT called during parse; the clap rounding helpers run at decode time when applying the clap transform. On 78638f4f with default security limits the oversized clap/ispe geometry is rejected by the image-size limit ("Image size … exceeds the maximum image size 1073741824") BEFORE the clap rounding executes, so the documented PoC never reaches the assert via the DECODE path. RECLASSIFIED 2026-06-26: the same assert IS reachable through a different public API — the tiling path heif_image_handle_get_image_tiling(…, process_image_transformations=1), which DOUBLE-applies clap (m_width is already 0 from load → process_image_transformations_on_tiling → Box_clap::left_rounded(0) → Fraction(0u-1u, 2) = Fraction(0xFFFFFFFF, 2) → assert), bypassing the decode-time size limit. Confirmed and patched upstream as GHSA-jc8f-p23p-5hjg (libheif v1.23.1).
Vuln classinteger-overflow
CVE
CVSS
Discovered2026-05-03

Verification

Evidence Static only (not real-world verified)
Harness fired❌ no
Protocol2.0
Sanitizernone
Crash typereachable assertion/SIGABRT (debug) or silent int32 narrowing (release) via the tiling API — per upstream advisory GHSA-jc8f-p23p-5hjg; not reproduced in-house (our decode-path PoC is size-limited)
Repro2026-06-22 verdict: built a pristine 78638f4f Debug ASan/UBSan lib (asserts confirmed ON — LIBHEIF-0009's assert(false) fires in the same lib). Ran the documented F3-fuzz-423b.bin through the public C API (heif_context_read_from_file -> heif_context_get_primary_image_handle -> heif_decode_image). Parse SUCCEEDS (primary reported 0x1); decode then returns the graceful security-limit error "Image size 4194304064x64 exceeds the maximum image size 1073741824" — the Fraction(uint32_t,uint32_t) assert at box.cc:85 is NEVER reached. The assert ctor is invoked only by the clap rounding helpers (decode-time), which run after the image-size security check that intercepts the oversized geometry. Hence the documented debug-SIGABRT repro does not reproduce on this build and the bug is not attacker-reachable through the public DECODE API with default limits. UPDATE 2026-06-26 — RECLASSIFIED false-positive → confirmed: the same assert IS reachable through the tiling API (heif_image_handle_get_image_tiling, which double-applies clap), now published + patched upstream as GHSA-jc8f-p23p-5hjg (libheif v1.23.1). Reachability was established by the upstream advisory (tiling path), not reproduced in-house — hence verification.verified stays false while status is confirmed.

Reproduce / self-verify

# build
cmake ASan/UBSan Debug libheif @ 78638f4f, clang-20 (asserts ON; reuse project/builds/unc_asan)
# run
./decode_file F3-fuzz-423b.bin   # public API heif_context_read_from_file -> heif_decode_image
# expect (none):
(historic claim) box.cc:85 Assertion 'num <= INT32_MAX' failed — NOT reproduced on 78638f4f (see verdict)

cost: low

Disclosure

Reported toGitHub security advisory GHSA-jc8f-p23p-5hjg — strukturag/libheif
Reported
Vendor ack
Embargo until
Public2026-06-26
Patched inv1.23.1

PoC: project/blogpost/findings/F3-fraction-uint32-assert/F3-fuzz-423b.bin (423-byte HEIF, in /home/ariel/Projects/libheif)

Writeup

Summary

Fraction::Fraction(uint32_t, uint32_t) (libheif/box.cc:83-89) asserts that both operands are <= INT32_MAX, then narrows to int32_t. Two attacker paths reach it from a parsed clap (clean-aperture) box: (a) cleanApertureWidth_num

INT32_MAX, and (b) image_width == 0 (missing ispe) makes image_width - 1U underflow to UINT32_MAX. In a debug build this SIGABRTs on parse; in release/NDEBUG the assert is compiled out and int32_t(num) silently wraps.

Reproduction

423-byte fuzz-derived HEIF with a malformed clap. Debug build → box.cc:85: Assertion 'num <= INT32_MAX' failed. Release build → rc=0, silent wrap (no downstream memory-safety effect observed with this reproducer).

// libheif/box.cc:83-89
Fraction::Fraction(uint32_t num, uint32_t den) {
  assert(num <= (uint32_t) std::numeric_limits<int32_t>::max());   // line 85
  assert(den <= (uint32_t) std::numeric_limits<int32_t>::max());
  *this = Fraction(int32_t(num), int32_t(den));
}

Call sites: Box_clap::left_rounded/right_rounded/top_rounded/bottom_rounded (box.cc:3629-3662), which also compute Fraction(image_width - 1U, 2U).

Root cause

uint32→int32 narrowing (CWE-191 underflow feeding a narrowing conversion) with a debug-only assert as the sole guard. Suggested fix: forward to the existing Fraction(int64_t,int64_t) constructor at box.cc:91+, which already clamps out-of-range values into int32 range.

Impact

Per the impact rubric: low (robustness/hardening). SIGABRT in debug builds; a silent narrowing wrap in release builds with no observed memory-safety effect for the reproducer we have. Held back from the security advisory and reported as a hardening item. Still present in v1.22.0 (and master @ b5ccea15) — the asserts are unchanged; a saturating-clamp fix was suggested upstream.

Verdict — CONFIRMED (overturned 2026-06-26)

Overturned 2026-06-26. The same box.cc:85 assert is reachable through a different public API — the tiling path heif_image_handle_get_image_tiling(…, process_image_transformations=1), which double-applies clap: m_width is already 0 after load, so process_image_transformations_on_tiling() calls Box_clap::left_rounded(0)Fraction(0u - 1u, 2) = Fraction(0xFFFFFFFF, 2) → assert. That is exactly the image_width==0 underflow this verdict anticipated below, and it bypasses the decode-time image-size limit. Published + patched upstream as GHSA-jc8f-p23p-5hjg (libheif v1.23.1; reporter Feng Ning / Innora Security). Reclassified false-positive → confirmed. The decode-path analysis below is still correct (that path is size-limited) — the tiling path is what makes it reachable.

Original decode-path verdict (2026-06-22) — false-positive for the decode path

Re-verifying up the evidence ladder on a pristine 78638f4f Debug ASan/UBSan build (asserts confirmed ON — see [[LIBHEIF-0009]], whose assert(false) fires in the very same library), the documented F3-fuzz-423b.bin PoC does not reproduce the claimed box.cc:85 SIGABRT through the public API. The full sequence is:

  1. heif_context_read_from_file parses the file with no assert (parse does not construct Fraction(uint32_t,uint32_t)).
  2. heif_decode_image then returns a graceful error Image size 4194304064x64 exceeds the maximum image size 1073741824 — the image-size security limit rejects the oversized clap/ispe geometry.
  3. The asserting ctor is reached only by the Box_clap::*_rounded helpers, which run while applying the clap transform — i.e. after the security check that already rejected the input.

So with default security limits no realistic public-API consumer drives the assert with this PoC → false-positive per the verification protocol (the oracle does not fire through a realistic consumer). The underlying code smell is real and unchanged (the uint32_t ctor still asserts/narrows; the int64 ctor is the safe sibling), so it remains a worthwhile upstream hardening item — but it is not an attacker-reachable defect through the decode path on this build. A future session wanting to overturn this must craft an under-limit input that still drives a clap dimension > INT32_MAX (or the image_width==0 underflow) past the security check and show the assert/narrowing firing through heif_decode_image.

References