A single afternoon of fuzzing against libheif 1.21.2 produced two memory-safety bugs in the same function. The first is a NULL pointer dereference on a malformed grid dimg reference - deterministic denial of service on any consumer that calls heif_decode_image or heif_image_handle_decode_image_tile. The second is a uint32 underflow in the inverse-rotation tile arithmetic that feeds a debug-only assert in the grid index lookup; in NDEBUG release builds (the configuration typical distribution packages use) the assert is compiled out and the access becomes a heap out-of-bounds read with an attacker-influenced offset. Disclosed privately to the maintainer on 2026-05-02 and fixed in libheif 1.22.0, released 2026-05-19. Tracked as GHSA-6x5f-qchq-cxqv and assigned CVE-2026-48029.
Discovery Context
How the bug was found.
Two memory-safety bugs in the same function, two lines apart. F1 is a missing null check after HeifContext::get_image(); F2 is a uint32 underflow upstream feeding an unchecked vector access gated only by a debug-only assert.
libheif is the de-facto open-source HEIF/HEIC/AVIF container library. If you have ever opened a .heic from an iPhone on Linux, viewed an AVIF in a desktop image viewer, processed an iPhone export with ImageMagick or libvips on a server, or hit an Android app that reads HEIC, you have very likely run libheif code. The container is intricate - ISO/IEC 23008-12 layered on top of the ISO Base Media File Format - the attack surface is exactly what you would expect for a media parser, and the consumer base is large.
This session was a single afternoon against the 1.21.2 release tag (commit 78638f4f), with two working assumptions. The first: the container layer runs before any codec back-end, so any container-side bug is reachable on hosts that do not have libde265 or libaom installed at all. The second: the derived-image layer - grid, overlay, iden, tiled, mask - sits on top of the container and mixes parsed header data with cross-references to other items in the file. Cross-references between two independent parsed data sources are historically a rich source of arithmetic and lifetime mistakes.
The harness that produced both findings - decode_grid_overlay - is a 200-line libFuzzer driver. It walks every top-level image, calls get_image_tiling with both process_xforms=0 and =1, probes seven (col, row) coordinate pairs per tiling layout (corners, centres, quarter-points), runs a per-tile decode, then a full-grid composition decode, then any auxiliary images. The same harness was linked against two libheif builds: a debug ASAN+UBSAN build with asserts on, and a release -O2 -DNDEBUG ASAN+UBSAN build with asserts compiled out. That second build is the choice that mattered most in this session.
Two grid-decode signatures fired in the first thirty seconds of the first sanity run. Both landed in ImageItem_Grid::decode_grid_tile, two lines apart, but with completely different failure modes and completely different root causes. Triage, root-cause derivation, sibling-class audit, suggested fixes, and the private report to the maintainer all went out the same day.
The bugs are not exotic. The workflow is the interesting part. Across May 2, a cold clone of the repository turned into two libheif builds, the harness suite, two minimised reproducers, the sibling-class audit table, and a coordinated private report on the maintainer's desk. The local campaign continued across the May 2/May 3 window with overnight runs that confirmed a second F2 sink, and culminated in a chaos object-graph campaign a few days later that produced 30.4 million executions and zero new crashes. The closing section of this page returns to the workflow that made that pace possible.
Impact Analysis
Severity
High
Heap out-of-bounds read with an attacker-influenced offset, reachable through standard libheif decode APIs. GHSA-6x5f-qchq-cxqv rated High.
Status
Public / Patched
Both bugs fixed in libheif 1.22.0, released 2026-05-19. 17 days from private report to fixed release.
CVE
CVE-2026-48029
Assigned via GitHub Security Advisory GHSA-6x5f-qchq-cxqv on 2026-05-20.
Reachability
Public decode APIs
heif_decode_image and heif_image_handle_decode_image_tile - the most common entry points for libheif consumers.
Technical Breakdown
What is happening, in detail.
Both bugs live in libheif/image-items/grid.cc in the function ImageItem_Grid::decode_grid_tile. The relevant excerpt from libheif 1.21.2 is short:
grid.cc · 1.21.2
uint32_t idx = ty * m_grid_spec.get_columns() + tx;
assert(idx < m_grid_tile_ids.size()); // line 586 -- F2 sink
heif_item_id tile_id = m_grid_tile_ids[idx]; // line 588 -- F2 OOB read
std::shared_ptr<const ImageItem> tile_item =
get_context()->get_image(tile_id, true); // line 589
if (auto error = tile_item->get_item_error()) { // line 590 -- F1 NULL deref
return error;
}
Four consecutive lines, two distinct memory-safety bugs.
F1 - NULL pointer deref on a missing grid tile reference
A HEIF grid is a derived image: a virtual item that says "to draw me, take these N tile items, lay them out in an R×C grid." The tile items are pointed to via an iref dimg (derived-image reference). At parse time the only consistency check is that dimg.size() == rows * cols. A file that declares a perfectly-sized dimg list whose entries point at item ids that do not exist - or at ids that exist but are not decodable images (an Exif metadata item, for example) - passes that check fine.
At decode time, HeifContext::get_image(id, only_images) returns an empty std::shared_ptr for any id that does not resolve to an image item in the registry. The very next line in decode_grid_tile calls tile_item->get_item_error() on it - a member call through a null shared_ptr. SIGSEGV. The dereferenced address is 0x0 plus a small constant vtable offset, so this is a deterministic crash, not a controlled write.
The sibling derived-image classes all do the missing null-check:
File and line
Calls get_image(_, true)
Null-checks the result?
image-items/grid.cc:589
yes
no - this is the bug
image-items/overlay.cc:338
yes
yes (line 339)
image-items/iden.cc:86
yes
yes (line 87)
image-items/iden.cc:108
yes
yes (line 109)
The fix is structurally identical to the pattern at overlay.cc:339: five lines, one if (!tile_item) return Error{...};. That is what upstream commit e1b97646 shipped.
F2 - uint32 underflow into an unchecked vector index
F2 is the more interesting bug. The end of the chain is the line two above F1, on grid.cc:586-588: a uint32_t idx computed from the tile coordinates, an assert(idx < m_grid_tile_ids.size()), and then an unchecked std::vector::operator[] access.
In a debug build the assert fires and the process aborts. In a release build compiled with -DNDEBUG - the configuration typical distribution packages use - the assert is compiled to nothing and the next line walks a vector with an attacker-influenced index. ASAN reports:
AddressSanitizerrelease -O2 -DNDEBUG ASAN+UBSAN build of libheif 1.21.2
ASAN
==NN==ERROR: AddressSanitizer: SEGV on unknown address 0x610400000e00
==NN==The signal is caused by a READ memory access.
#0 ImageItem_Grid::decode_grid_tile grid.cc:588:26
#1 ImageItem_Grid::decode_compressed_image grid.cc:224
#2 ImageItem::decode_image image_item.cc:747
#3 HeifContext::decode_image context.cc:1404
#4 heif_image_handle_decode_image_tile heif_tiling.cc:108
SUMMARY: AddressSanitizer: SEGV grid.cc:588:26 in
ImageItem_Grid::decode_grid_tile(...)
ASAN reports SEGV rather than heap-buffer-overflow only because idx is so large that m_grid_tile_ids.data() + idx*4 overshoots into unmapped memory. The bug class is still the same: an unchecked vector::operator[] access with an attacker-influenced index. The bytes read are a heif_item_id (4 bytes), used downstream as a lookup key in HeifContext::get_image().
The huge index comes from a uint32 underflow upstream. ImageItem::transform_requested_tile_position_to_original_tile_position performs subtractions like num_columns - 1 - tile_y and num_rows - 1 - tile_x on caller-supplied coordinates, against the pre-rotation grid extents. The caller's (tile_x, tile_y) are documented as being in the post-rotation (displayed) grid. When the rotation is 90° or 270° and rows ≠ columns, the displayed grid has its dimensions swapped relative to the file grid, and a legal post-rotation coordinate can exceed the smaller pre-rotation extent. The subtraction underflows to ~UINT32_MAX, the underflowed value flows into decode_grid_tile, the idx = ty * cols + tx arithmetic produces an enormous index, and the vector access walks off the heap.
The caller supplies (tile_x, tile_y) in the displayed grid; the inverse-rotation arithmetic subtracts against the pre-rotation extents. When the rotation swaps rows and columns and the grid is non-square, one coordinate exceeds the smaller pre-rotation dimension and the uint32 subtraction underflows to ~UINT32_MAX. That value is then used as a vector index, gated only by a debug-only assert.
This is a CWE-191 (integer underflow) feeding a CWE-125 (out-of-bounds read). The primitive is not trivially-leaking into an external observer - the 4 bytes read are consumed as a hash-map lookup key - but it is structurally a memory-safety bug, not a robustness assertion. The advisory states exactly that scope and nothing more.
Why two builds matter
The same source code, the same input file, two different categorisations depending on whether -DNDEBUG is set:
Build configuration
F2 result
Debug ASAN+UBSAN, asserts on
SIGABRT via assert(idx < m_grid_tile_ids.size()) at grid.cc:586 - looks like a robustness assert
Release-style builds with assertions disabled are what most downstream packages ship, so that configuration is what defines the security envelope. A fuzzing setup that only runs against debug-with-asserts will look at the SIGABRT from F2 and file a hardening PR. The release-build re-run is what turns it into a memory-safety report.
Two fixes, not one
The patch above shows the F1 null check and the F2 sink-side runtime bounds check. A third commit (e523ec0b, authored by Dirk Farin) is the structurally correct F2 source-side fix - process transformations on the tiling upfront so the arithmetic operates on post-rotation dimensions, then reject out-of-range caller coordinates before any subtraction. The in-code comment matches the advisory almost word-for-word: the displayed grid has its columns and rows swapped relative to the file; using the file dims both let out-of-range coordinates through and produced unsigned underflows inside the inverse-rotation formulas.
Attribution and parallel discovery
An attribution note worth making explicit: two of the three upstream commits in this area - e1b97646 and 518bd95f - were authored by Anthony Hurtado with a "Found by: AFL++ fuzzing with custom harness" trailer. My private report and reproducers were sent to the maintainer before I knew those commits existed; when the maintainer eventually checked my PoC against v1.22.0, he confirmed it was already fixed - probably from his own fuzzer runs. The cleanest framing is parallel discovery and overlapping validation, not a claim that my report alone caused every upstream patch. The third commit, e523ec0b - the structurally correct F2 source-side fix - was authored by the maintainer (Dirk Farin) the following day, and its in-code comment matches the underflow scenario described in my advisory almost word-for-word. The advisory itself (GHSA-6x5f-qchq-cxqv) was accepted, published, and credited to me as reporter after I mirrored the report to GitHub on 2026-05-20.
Patchlibheif/image-items/grid.cc - upstream fixes in v1.22.0
diff
@@ ImageItem_Grid::decode_grid_tile @@ uint32_t idx = ty * m_grid_spec.get_columns() + tx;- assert(idx < m_grid_tile_ids.size());+ if (idx >= m_grid_tile_ids.size()) {+ return Error{heif_error_Invalid_input,+ heif_suberror_Missing_grid_images,+ "Grid tile coordinate out of range"};+ } heif_item_id tile_id = m_grid_tile_ids[idx]; std::shared_ptr<const ImageItem> tile_item = get_context()->get_image(tile_id, true);+ if (!tile_item) {+ return Error{heif_error_Invalid_input,+ heif_suberror_Missing_grid_images,+ "Grid tile references a non-existent item"};+ } if (auto error = tile_item->get_item_error()) { return error; }
Reachability and Impact
What an attacker can actually do.
The realistic blast radius is wider than for the average codec bug, because both bugs live in the container layer - which runs before any codec back-end - and are reachable through libheif's most-used public APIs. Any consumer that opens a malicious HEIF or HEIC file and asks libheif to decode either the primary image or a single tile reaches the vulnerable function.
Surface
Verdict
Why
heif_decode_image on a malicious grid HEIF
Confirmed reachable
Full-grid composition iterates per-tile and lands in decode_grid_tile. Either F1 or F2 fires depending on the file shape. ASAN reproducers in both release and debug builds.
heif_image_handle_decode_image_tile
Confirmed reachable
Direct single-tile decode path. F2 fires here when the file carries an irot property and rows ≠ columns; F1 fires here when the dimg list references missing or non-image items.
heif_image_handle_get_grid_image_tile_id
Reachable for F2
Invokes the same transform; the second F2 sink confirmed during the post-disclosure overnight fuzz run.
Image viewers and thumbnailers on Linux/BSD (gThumb, geeqie, Nautilus thumbnailers, KIO)
Plausible direct exposure
These tools call heif_decode_image or its convenience wrappers on user-supplied files. A malicious file in a downloads folder is enough to crash the previewer or thumbnailer.
Server-side image pipelines that link libheif (ImageMagick, libvips when built with HEIF support, custom transcoders)
Plausible direct exposure
Any pipeline that accepts user-uploaded HEIC and runs heif_decode_image can be crashed on demand. F2's OOB read does not leak into an external observer by itself, but reliable DoS-on-decode is enough to matter for upload pipelines.
Mainstream browsers ship their own AVIF stacks (dav1d, libavif) rather than libheif. WebAssembly-shipped libheif builds in some image-editor apps would be reachable, but the WASM sandbox contains the OOB read to the module's linear memory - it does not directly compromise the browser process.
Read-only consumers that never call a decode API
Not affected
The bugs are in the decode path. Container-only inspection (metadata enumeration, item listing) does not reach decode_grid_tile.
Exploitability
F1 - reliable DoS. Any consumer that calls heif_decode_image or heif_image_handle_decode_image_tile on a malicious file with a malformed dimg reference SIGSEGVs. The dereferenced address is 0x0 plus a small constant vtable offset - deterministic across runs and platforms, not attacker-controllable.
F2 - DoS plus a heap OOB read primitive. The 4 bytes read at the attacker-influenced offset are consumed as a heif_item_id lookup key in HeifContext::get_image(). By itself this is not a trivially-leaking primitive into an external observer; in a larger gadget chain (combined with an info-leak side channel via caching behaviour or timing) it could matter. Exploitability beyond the sanitizer report was not pursued.
Memory write. Neither bug provides a write primitive.
Sandbox guidance. Out-of-process decode workers are the right mitigation for downstream consumers that cannot immediately update to 1.22.0. Both bugs result in SIGSEGV in release builds, which a try/catch in the consumer process will not catch.
Disclosure Timeline
From discovery to coordinated disclosure.
2026-05-02, morningCloned libheif 1.21.2 (commit 78638f4f). Read the container, derived-image, and metadata code top-down. Wrote a short attack-surface note prioritising the container and derived-image layers because they run before any codec back-end and are reachable from every consumer.
2026-05-02, afternoonFive small libFuzzer harnesses written: decode_primary, decode_metadata, decode_thumbnail, decode_grid_overlay, decode_sequence. Built libheif twice - debug ASAN+UBSAN, and release -O2 -DNDEBUG ASAN+UBSAN. The second build is what later turned a robustness assert into a memory-safety report.
2026-05-02, afternoonFirst sanity-fuzz run on decode_grid_overlay. Within 30 seconds, two distinct signatures fire: a NULL deref at grid.cc:590 and an assertion at grid.cc:586.
2026-05-02, eveningTriage complete on both signatures. Sibling-class audit shows overlay.cc, iden.cc all do the missing null-check; grid.cc is the outlier. The grid.cc:586 assertion is debug-only - the release-NDEBUG build re-run confirms an ASAN-reported heap OOB read.
2026-05-02, eveningPrivate security report sent to Dirk Farin (libheif maintainer) by email. Bundle includes a 303-byte minimised reproducer for F1, a 16,389-byte reproducer for F2, root-cause writeup, suggested fixes for both, and a request to coordinate fix and embargo through the advisory.
2026-05-03 to 2026-05-06Local campaign continued across the May 2/May 3 window and beyond: post-disclosure overnight fuzz runs confirmed a second F2 sink (also reachable via heif_image_handle_get_grid_image_tile_id), then a chaos object-graph campaign (612 chaos seeds, 50 size-stress seeds, 200 box-level mutations). 30.4 million executions, zero new crashes. Coverage on decode_grid_overlay rose from 25,807 to 28,884 edges - a strong negative result.
2026-05-17, 22:01 -05:00Upstream commit e1b97646 ("grid: fix NULL deref in decode_grid_tile on missing tile reference") authored by Anthony Hurtado. Trailer: "Found by: AFL++ fuzzing with custom harness". The fix shape matches the sibling-class pattern recommended in the advisory.
2026-05-17, 22:02 -05:00Upstream commit 518bd95f ("grid: fix OOB read in decode_grid_tile when grid exceeds tile count") authored by Anthony Hurtado. Replaces the debug-only assert with a runtime error return - the F2 sink-side fix.
2026-05-18, 17:46 +02:00Upstream commit e523ec0b ("fix tile coordinates validation in rotated images") authored by Dirk Farin (libheif maintainer). The structurally correct F2 source-side fix: process transformations on the tiling upfront, validate caller coordinates against post-rotation bounds, track current dimensions through the property chain. The in-code comment matches the underflow scenario described in the advisory almost word-for-word.
2026-05-19, 19:06 +02:00libheif v1.22.0 tagged. All three fix commits included.
2026-05-20, 00:23Follow-up email to maintainer (the original disclosure had been overlooked in the email triage). Reply 14 minutes later: "I just checked your PoC and found that it is already fixed. Probably from my own fuzzer runs. It just went into the release v1.22.0 today." Mirrored the report to GitHub as GHSA-6x5f-qchq-cxqv.
2026-05-21Reproducers re-run against a fresh v1.22.0 release-NDEBUG ASAN+UBSAN build with codec back-ends off. Both reproducers exit clean - no sanitizer signal in release or debug. Public writeup published.
Takeaways
Lessons.
Asserts are not bounds checks. Anywhere a parsed-from-the-file integer is used as an array index or a pointer offset, the check must be a runtime if that returns an error in release builds. assert() is documentation, not protection. F2 looked like a robustness signal in the debug build and a memory-safety report in the release build - same source, same input, same line, different categorisation. Build and fuzz the release-mode ASAN configuration explicitly.
Cross-references between two parsed sources are bug factories. The grid header says "R×C tiles." A separate parsed list says "and here are the N tile item ids." A separate part of the file says "and the image is rotated 270 degrees." The decode path has to reconcile all three with the caller's coordinates - which themselves arrive through a public API. F2 is exactly the pattern of one parsed value flowing into arithmetic against a different parsed value. Whenever a header declares a count or a shape and a separate parsed list provides the elements, validate at parse time that the two agree and re-check at use time.
Trust boundaries include caller-supplied API arguments.heif_image_handle_decode_image_tile(handle, tile_x, tile_y) is a public API. The coordinates come from the consumer, not the file - but consumers typically derive them from parsed-from-the-file dimensions returned by get_image_tiling(). Either way, libheif has to validate them itself against the displayed grid before they enter inverse-rotation arithmetic. The source-side fix (commit e523ec0b) closes exactly this gap.
Sibling-class audit before reporting. The most effective sentence in a security report is "you already do this everywhere else, except here." For F1, the four sibling derived-image classes (overlay.cc, two sites in iden.cc) already do the missing null-check. That makes the fix a five-line patch with a structural precedent rather than a debate about whether the input was valid in the first place. The audit takes ten minutes; it saves the maintainer hours and the reporter a round-trip.
Structurally-aware seeds beat random mutation on framed formats. ISO BMFF box framing eats random byte mutations alive - most mutants produce invalid lengths and stop parsing before reaching anything interesting. A 25-seed generator that produces deliberately-invalid-but-parseable HEIFs (grids pointing at missing items, grids pointing at non-image items, size mismatches, irot+imir combinations, extreme dimensions) hit F1's exact 303-byte shape within thirty seconds. The bug class wants a structurally valid container with an inconsistent cross-reference - that is exactly what targeted seeds produce.
Methodology
A two-model research loop.
The work that produced this disclosure was not one agent grinding through libheif alone. It was a deliberate two-model loop with a human in the conductor seat: one model for execution, one for critique, the human choosing what to commit to.
The execution model (Claude) did the work that takes wall-clock time. Reading the libheif source top-down. Drafting the attack-surface note. Writing the five harnesses. Building the debug and release-NDEBUG libraries. Running the sanity fuzzes. Triaging the two grid signatures. Composing the sibling-class audit table. Minimising F1 to 303 bytes. Authoring the suggested-fix diffs. Drafting the private security report. End to end.
The execution model does the work that takes wall-clock time. The critic model is kept off the execution path on purpose, so its pushback is independent of the work it is critiquing. The human picks the target and commits to actions.
The critic model (ChatGPT) was kept off the execution path on purpose - its job was to challenge conclusions, not author them. The most useful intervention in this campaign was the moment the execution side declared libheif "saturated" after a clean overnight run. The critic pushed back: structured harness fuzzing is complete, but you have not tested chaotic cross-object composition - semi-valid containers with conflicting item references, derived-image-of-derived-image, transforms stacked on the wrong items, iloc extents pointing into parseable-but-weird regions. It produced a full Phase 7 plan with an explicit stopping criterion. The execution side built it: 612 chaos seeds plus 50 size-stress seeds plus 200 box-level mutations, run as smoke, short, and overnight tracks. The result was zero new crashes across 30.4 million executions - but with measurable coverage gain over the prior best, decode_grid_overlay edges climbing from 25,807 to 28,884 (+11.9%) and decode_primary from 25,399 to 27,289 (+7.4%). A strong negative result. Without the critic, that phase would have been rationalised away as diminishing returns and the saturation claim would have rested on absence of evidence rather than evidence of absence.
The same loop ran during disclosure. The first email to the maintainer was drafted by the execution side; the critic edited it for tone (less apologetic, more practical) and recommended the shorter of two phrasings for the CVE-request follow-up. Small edits, real effect on the response.
The human role in the loop is narrower than it sounds and harder than it looks: pick the target, accept or reject the critic's pushback, commit to the actions the execution side proposes, decide when to disclose and how. The models are fast; the judgment about which question to ask is the part that does not scale yet.
This research was done over a weekend, on my own free time, outside work hours, and not as part of my job.
Closing thought
AI is an amplifier of intent.
What collapsed is not the difficulty of finding bugs. It is the cost of acting on intent. Each step of vulnerability research - reading code top-down, hypothesising where a bug class lives, building a harness, triaging a crash, doing a sibling-class audit, drafting a clean report with a suggested fix - used to be a manual sequence punctuated by hours of human attention. Now most of those steps are reasoning-over-actions that an agent can execute in seconds against a fully-grounded view of the codebase. The intent here was mine; the wall-clock cost was a fraction of what the same intent used to cost.
The uncomfortable corollary is that today's models are the worst versions of these models we will ever use again. Whatever the bottleneck looks like at this point in 2026, it will be smaller next quarter and smaller still the quarter after. For maintainers, that means the bar for "we would have caught this in code review" is rising faster than the codebase. For defenders, it means the inventory of latent bugs in mature libraries is going to be drained faster than the disclosure infrastructure was designed to handle. For researchers, the leverage now sits less in the act of finding a bug and more in the choice of where to point. The disclosure cadence libheif demonstrated here - 17 days from private report to fixed release, with the maintainer himself authoring the structurally correct F2 source-side fix - is the kind of pace the rest of the ecosystem should be optimising toward.
The bugs are arithmetic mistakes. The story is the timeline.
Realistic Exposure
Who is actually at risk?
libheif is the canonical open-source HEIF/HEIC/AVIF container library. Direct downstream consumers include ImageMagick, libvips, GIMP, KDE's KImageFormats, GNOME's image viewers and thumbnailers, and a long tail of Linux desktop image apps. The ecosystem reach is large; the realistic exposure to these specific bugs is narrower, because four gates must all be true: (1) the consumer links a libheif at or before 1.21.2 with the grid-decode path enabled; (2) it actually calls a decode API on user-supplied input; (3) for F2, the input can carry an irot property and a non-square grid; (4) the host has not already received a libheif update through its distribution. Most distributions track libheif closely, so the practical exposure window is the time between v1.22.0 reaching their mirrors and downstream consumers picking up the rebuilt package.
Platform / category
Assessment
Why
Linux desktop image viewers and thumbnailers (gThumb, geeqie, Nautilus, KIO, GNOME image viewer, KDE Gwenview)
Plausible direct exposure
These tools link libheif and call heif_decode_image on user-supplied files. A malicious HEIC in a downloads folder is enough to crash the previewer or thumbnailer. DoS-on-decode is the realistic outcome.
Any pipeline that accepts user-uploaded HEIC and runs libheif's decode APIs can be crashed on demand. F2's OOB read does not leak into an external observer by itself, but reliable DoS-on-decode of an upload worker still matters for upload-driven services.
Mobile platforms (iOS Photos, Android Gallery, vendor camera apps)
Not affected
iOS uses its own ImageIO/HEIF stack. Android relies on platform decoders that do not link libheif. The HEIC ecosystem on mobile bypasses libheif entirely.
Major browsers (Chrome, Firefox, Safari, Edge)
Reachability not confirmed
Mainstream browsers ship libavif/dav1d for AVIF and platform decoders for HEIC, not libheif. Image-editor PWAs that ship libheif in WebAssembly would be reachable, but a WASM sandbox contains the OOB read to the module's linear memory - it does not directly compromise the browser process.
Linux distributions (Debian, Ubuntu, Fedora, Arch, openSUSE)
Update path is the mitigation
Distributions track libheif closely. The right action is to wait for the distro update that pulls 1.22.0 and rebuild any consumers that statically link libheif. Backporting the three fix commits (e1b97646, 518bd95f, e523ec0b) is small and local. See the packaging-status snapshot below for which distributions have caught up.
Custom builds of libheif compiled with -DCMAKE_BUILD_TYPE=Debug
Different failure mode
Debug builds with asserts on will SIGABRT instead of producing a heap OOB read on F2. Still a denial of service; not a memory-safety report. The decision to fuzz the release-NDEBUG configuration is what surfaced F2 as memory-safety.
Realistic exposure
This does not mean every browser or every phone is affected. Many mainstream browsers and mobile platforms use different AVIF/HEIF decode paths or platform media frameworks rather than libheif. The realistic exposure is Linux desktop software (image viewers, thumbnailers, file managers), server-side image pipelines and media-processing tools that link libheif, and distributions that package libheif directly. F2's heap OOB read primitive in release-style builds is structurally significant under standard exploit assumptions but does not by itself prove exploitation against any deployed application.
Packaging status as of 2026-05-21
The patch shipped two days ago. The table below is a static snapshot of Repology captured on the day of publication, frozen so it remains a faithful record of where the ecosystem stood when this advisory went out. Sage cells are patched (libheif 1.22.0); red cells are still vulnerable. Of 113 tracked distributions and repositories, 16 had picked up 1.22.0; 97 were still on a vulnerable version. The full list is shown below for record-keeping; the headline numbers above are the takeaway, and a future revision of this page may collapse the full list behind a "show all distributions" toggle.
Distribution
Version
Distribution
Version
Distribution
Version
Alpine Linux 3.21
1.19.5
Fedora 43
1.20.2
Parrot
1.19.8
Alpine Linux 3.22
1.19.8
Fedora 44
1.21.2
PCLinuxOS
1.15.1
Alpine Linux 3.23
1.21.2
Fedora Rawhide
1.21.2
Pisi Linux
1.21.2
Alpine Linux Edge
1.21.2
FreeBSD Ports
1.21.2
pkgsrc-2025Q4
1.20.2
ALT Linux p9
1.6.2
Gentoo
1.21.2
pkgsrc-2026Q1
1.21.2
ALT Linux p10
1.19.5
GNU Guix
1.19.7
pkgsrc current
1.21.2
ALT Linux p11
1.21.2
HaikuPorts master
1.21.2
PLD Linux
1.21.2
ALT Sisyphus
1.22.0
Homebrew
1.22.0
PureOS amber
1.3.2
AOSC
1.21.2
Kali Linux Rolling
1.21.2
PureOS byzantium
1.11.0
Apertis v2025
1.15.1
KaOS
1.21.2
PureOS landing
1.19.8
Apertis v2026
1.19.8
KaOS Build
1.22.0
Raspbian Oldstable
1.15.1
Apertis v2027 Development
1.19.8
LiGurOS stable
1.20.1
Raspbian Stable
1.19.8
Arch Linux
1.22.0
LiGurOS develop
1.21.2
Raspbian Testing
1.21.2
ArchPOWER powerpc
1.21.2
MacPorts
1.21.2
Ravenports
1.21.2
ArchPOWER powerpc64le
1.21.2
Mageia cauldron
1.20.2
Rosa 2021.1
1.12.0
ArchPOWER riscv64
1.19.7
Manjaro Stable
1.21.2
Rosa 13
1.19.8
AUR
1.22.0
Manjaro Testing
1.21.2
RPM Fusion EL 8
1.7.0
Artix
1.21.2
Manjaro Unstable
1.22.0
Side Linux
1.21.2
Chimera Linux
1.20.2
MSYS2 clang64
1.22.0
SlackBuilds
1.20.2
Chromebrew
1.20.2
MSYS2 clangarm64
1.22.0
SliTaz Next
1.3.2
ConanCenter
1.20.1
MSYS2 mingw
1.22.0
Solus
1.21.2
CRUX 3.8
1.21.2
MSYS2 ucrt64
1.22.0
Spack
1.12.0
Cygwin
1.12.0
MX Linux MX-21 Testing
1.14.0
stal/IX
1.21.2
Debian 11
1.11.0
MX Linux MX-23 Testing
1.17.6
stal/IX dev
1.21.2
Debian 12
1.15.1
nixpkgs stable 25.11
1.20.2
T2 SDE
1.22.0
Debian 12 Backports
1.19.7
nixpkgs unstable
1.21.2
Termux
1.22.0
Debian 13
1.19.8
OpenBSD Ports
1.22.0
Trisquel 11.0
1.12.0
Debian 14
1.21.2
OpenIndiana packages
1.20.2
Ubuntu 18.04
1.1.0
Debian Unstable
1.21.2
openmamba
1.22.0
Ubuntu 20.04
1.6.1
deepin 20
1.3.2
OpenMandriva 6.0
1.19.7
Ubuntu 22.04
1.12.0
deepin 23
1.18.1
OpenMandriva Rolling
1.21.2
Ubuntu 24.04
1.17.6
Devuan 4.0
1.11.0
OpenMandriva Cooker
1.21.2
Ubuntu 25.10
1.20.2
Devuan Unstable
1.21.2
openSUSE Tumbleweed
1.21.2
Ubuntu 26.04
1.21.2
Endless OS master
1.15.1
openSUSE multimedia:libs Tumbleweed
1.21.2
Ubuntu 26.10
1.21.2
EPEL 9
1.16.1
PackMan openSUSE Tumbleweed
1.21.2
Ubuntu 26.10 Proposed
1.21.2
EPEL 10
1.17.6
PackMan SLE 15
1.21.2
Vcpkg
1.21.2
Exherbo
1.22.0
Parabola
1.22.0
Void Linux x86_64
1.21.2
Fedora 42
1.19.8
Pardus 21
1.11.0
The vast majority of tracked distributions and repositories in this snapshot still packaged a libheif version that contained both bugs. The patch is local to two functions and small enough to backport, but for most users the realistic path is to wait for the distribution update.
What to do
Update to libheif 1.22.0. The three fix commits are local to two functions and small enough to backport.
If you ship a static build of libheif, rebuild against 1.22.0 and re-link consumers.
For service operators: a libheif decode worker that crashes mid-request should restart cleanly, not poison a longer-lived shared process. Out-of-process decode is the right boundary regardless of this specific bug.