v1.23.4 is a security and bugfix release. It is ABI- and API-compatible with v1.23.3 and is a drop-in replacement.
Three of the fixed issues are rated high, so all users are advised to upgrade.
Security fixes
(CVE numbers will be added when assigned.)
- CVE-2026-XXXXX (GHSA-vg7w-rp49-4fc2) The max_items security limit was not enforced for the child boxes of iinf, so a file could declare an unbounded number of items. Parsing then cost quadratic time in the per-item iref reference scan. The limit is now checked up front, the declared count is bounded by the remaining input, and a short read is reported instead of silently yielding fewer children. The iref queries are indexed by source item ID rather than scanning the whole reference list, which removes the residual quadratic cost (on a file with 1000 items and 160000 dimg references, read time drops from 1.83 s to 0.26 s). In Emscripten builds, the same item count sized an alloca() in the JavaScript item-ID helpers, overflowing the 64 KB WebAssembly shadow stack into the static data segment and permanently trapping the module instance. (high)
- CVE-2026-XXXXX (GHSA-xrp2-63fq-jm8q) Unbounded recursion in the reference-cycle check crashed the parser on a long chain of derived items, without bound when the item-count limit is disabled. The check no longer runs at file load, and the decode-time walk that replaces it uses an explicit heap worklist instead of recursion. (high)
- CVE-2026-XXXXX (GHSA-prgh-72vc-3xmc) Permanent decoder deadlock through a lock-order inversion in parallel grid tile decoding (enabled by default). ImageItem::decode_image() holds a per-item non-recursive mutex across its nested decodes, so two workers whose items reference each other took the two mutexes in opposite order. Cyclic decode reference graphs, following both the dimg and the alpha auxl edges, are now rejected before decoding starts. (high)
- CVE-2026-XXXXX (GHSA-fqpw-fj22-78w4) Heap out-of-bounds read in the encoder plugins for images whose luma and chroma bit depths differ. The aom and x265 plugins took the sample width from the luma channel and applied it to the chroma planes, reading each chroma row at twice its allocated width and encoding those bytes into the output. Every encoder plugin now validates its input. (medium)
- CVE-2026-XXXXX (GHSA-4rv4-953r-p24q) Unreclaimable memory leak in heif_track_get_next_raw_sequence_sample(). Four error paths in the sample auxiliary-information stage dropped the already-copied sample payload, and the error return could not carry it back to the caller. Since nothing requires distinct trak boxes to reference distinct media bytes, a small file with many tracks aliasing one payload leaked it once per track and per call, uncharged to max_total_memory. The entry point also lacked the exception guard, so a failing allocation unwound std::bad_alloc across the extern "C" boundary and aborted the host process. (medium)
- CVE-2026-XXXXX (GHSA-rhgw-q5g8-xjh2) Heap out-of-bounds read in the WebCodecs decoder plugin. The pixel buffer was sized for the frame's visible rectangle but the copy loops walked it at the coded width and height, and the gap between the two is the HEVC conformance window that the decoded file controls. Only Emscripten builds with WITH_WEBCODECS=ON (off by default, experimental) that do not also link libde265 are affected. (medium)
Thanks to @hackerman70000 for reporting these issues.
Hardening
- Color conversion rejects images with components wider than 16 bits. Every conversion operator accesses planes through uint8_t* or uint16_t* and derives shifts from the bit depth, so a monochrome unci track with a 64-bit component reached Op_mono_to_YCbCr420 and shifted an int by 56 (undefined behavior, found by OSS-Fuzz). Each operator now declares its own bit-depth limit instead of only bounding the depth from below
- The sharp-yuv RGB to YCbCr 4:2:0 operator declines input whose alpha plane has a different bit depth than the color channels. It derived the alpha sample width from heif_channel_R, so 10-bit planar RGB next to an 8-bit alpha plane walked a one-byte-per-sample plane with a step of two (heap-buffer-overflow read, found by OSS-Fuzz)
- The number of iref reference entries is capped against max_items, matching the caps already applied in iloc, iinf and ipma. Only the per-entry reference count was bounded before
- heif_image_handle_get_depth_image_handle(), _get_thumbnail() and _get_auxiliary_image_handle() return the item's error at handle-creation time instead of handing out a handle for an item that failed to parse
- MIAF's derived-image dependency constraints (ISO/IEC 23000-22, clause 7.3.11) are validated before decoding for files that declare the miaf brand
Bug fixes
- heif_item_add_*_property() returned the index of the property box in the file-wide ipco container, while the property getters address the properties of a single item. The two numberings coincide only in a file with a single item, so reading a property back with the returned id indexed past the end of the item's property list, and the out-of-range check built an Error object without returning it. The id returned is now the position within the item's own property list (GHSA-5w8x-856j-7x7c, reported as a vulnerability but caused by an unsupported use of the API; it is a real crash in supported multi-item contexts)
- heif_track_encode_sequence_image() documents its options parameter as optional, but passing NULL dereferenced a null pointer, and a second dereference lurked on the same path in the color conversion options whenever the input actually needed converting
- A file containing a reference cycle no longer fails to load as a whole. The parse-time check rejected the file whenever the primary item's dimg subtree contained a cycle, which also made the file's other, independently valid items undecodable. Such a file now opens, its valid items decode, and only the cyclic item fails, at decode
- aom: lossless encoding (-L) failed with "Only --enable_chroma_deltaq=0 can be used with --lossless=1". libaom enables chroma delta-q for AOM_TUNE_IQ and then refuses to combine it with lossless coding, and libheif auto-selects that tune for still images. Lossless images keep AOM_TUNE_SSIM, and an explicit request for both is reported as the unsupported combination it is
- x264: 10-bit input was encoded as an 8-bit stream built from misread bytes. The plugin applied the profile names "main10-intra" and "main12-intra", which are x265 names that x264_param_apply_profile() rejects, and never set param.i_bitdepth or X264_CSP_HIGH_DEPTH. 10-bit input now produces a High 10 stream; 12 bits, which x264 does not support, is refused
- uvg266: images with a bit depth the build does not support are refused instead of silently producing a corrupted image (#1859)
Behavior changes
- Files with a reference cycle now load instead of being rejected at parse time; the cyclic item fails when it is decoded
- For files declaring the miaf brand, derived-image chains that violate ISO/IEC 23000-22 clause 7.3.11 are rejected before decoding
Other
- Updated the libaom build helper to v3.15.0
- The clap and grid regression tests no longer require a HEVC decoder, so they are skipped rather than failed on distributions that build libheif without the HEVC codecs (#1900)