v1.23.6 is a security and bugfix release. It is ABI- and API-compatible with v1.23.5 and is a drop-in replacement.
Four 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-h4h8-qgvc-m7r2) Memory exhaustion in the OpenJPEG decoder plugin through the tile and component counts of a JPEG 2000 codestream. OpenJPEG allocates its coding parameters for every tile, and for every component of every tile, while it reads the SIZ marker segment. This happened before the plugin could check anything, and OpenJPEG has no limit of its own (uclouvain/openjpeg#1111). A 1357-byte file declaring 65535 tiles of one pixel made it allocate 1.8 GB with 24 components and 4.7 GB with 121 components, whatever the security limits were. The plugin now reads the SIZ marker segment itself before it calls OpenJPEG and bounds the number of tiles and components by
max_number_of_tiles,max_components,max_memory_block_sizeand the size of the input. Found by OSS-Fuzz. (high) - CVE-2026-XXXXX (GHSA-fcmw-5764-7rq8) Memory exhaustion in the uncompressed (
unci) decoder with generic compression. For an image with one compressed unit per tile and tile-component interleave, the unit was read and decompressed again for every component and the results were concatenated, none of it charged to the memory budget. A 22 kB file with 256 components allocated more than 5 GiB with the default security limits. A per-tile unit is now fetched once, decompression stops as soon as more data than the tile size is produced, a unit that decompresses to less is rejected, and the tile data is charged to the memory limits. (high) - CVE-2026-XXXXX (GHSA-6fqc-p7r8-2g36) CPU exhaustion in the
uncidecoder with generic compression. When a compressed unit spans several tiles (the whole item, or the whole image of a component), the complete item was decompressed again for every tile, so decoding took time proportional to the number of tiles times the item size. A 1 kB file with a 512x512 image of 1x1 tiles kept a CPU busy for several minutes, and a valid 4096x4096 image with 64x64 tiles decoded 700 times slower than the same image without tiles. Such an item is now decompressed once and the image is exposed as a single tile. Images with row or pixel units keep their tiling, and decoding a tile only reads and decompresses the units of this tile. (high) - CVE-2026-XXXXX (GHSA-q7mw-2fmm-5q94) Heap out-of-bounds read in libsharpyuv through sample values above the bit depth of an image. The sharp YUV conversion passed 10 and 12 bit RGB planes to libsharpyuv, which uses each sample as an index into a gamma table that is sized for the bit depth. Out-of-range samples could come from a crafted file when it is decoded to YCbCr 4:2:0 (through the
uncimixed-interleave decoder or signed JPEG 2000 components, see GHSA-v5rj-g4wv-j5w3 below), or from an application that encodes an image with such values. The result was a crash, or unrelated memory influencing the output pixels. The sample range is now checked before the samples are handed to libsharpyuv. (high) - CVE-2026-XXXXX (GHSA-vv35-6hxg-95x8) The bit depth that an image handle reports could differ from the bit depth of the decoded image, so that an application reading the planes with the bit depth of the handle (e.g. 16-bit samples from a plane of 8-bit samples) read out of bounds. There were three ways to get there: a configuration box (
hvcC,av1C) that contradicts the bitstream; a grid whose tiles have different bit depths, where the tile that did not match was silently left out since v1.19.0 and the bit depth of the result depended on which decoding thread finished first; and overlay (iovl) images with more than 8 bits, which reported the bit depth of their first input image but were composed on an 8-bit canvas. The last one needs no crafted file. A decoded image that contradicts its handle is now refused, all tiles of a grid have to have the same format, and overlays are composed with the bit depth of their first input image. (medium) - CVE-2026-XXXXX (GHSA-8p2c-9wv4-rx4p) Null pointer dereference when decoding an image sequence in which a chunk of a visual track references a non-visual sample entry (for example an
mp4aentry in thestsdbox of apict,videorauxvtrack). The decoder of such a chunk was only guarded by anassert(). The track is now rejected when it is loaded. (medium) - CVE-2026-XXXXX (GHSA-86ch-j429-fpr2) With the FFmpeg decoder plugin, the image size limit was not reliably applied before an HEVC bitstream reached FFmpeg, so a file could make the decoder allocate buffers for a much larger picture than its
ispeproperty declares. The SPS parser skipped 56 instead of 88 bits for each sub-layer profile and failed on valid headers with sub-layer profiles, the size scan skipped every SPS that it could not parse, and the plugin did not pass the limit on to FFmpeg. An SPS that cannot be parsed is now an error, the plugin refuses NAL units that contain a start code prefix (so that FFmpeg sees the same NAL units that libheif checked), and the limit is given to FFmpeg asmax_pixels. Builds that decode HEVC with libde265 are not affected. (medium) - CVE-2026-XXXXX (GHSA-3gwx-cjv4-jr74) Reachable assertion when decoding a grid image whose tiles have an alpha plane of more than 16 bits, which
uncitiles can have since v1.22.0. The opaque fill value was computed with a shift that was only guarded byassert(alpha_bpp <= 16): builds with assertions aborted, builds without shifted by up to 64 bits (undefined behaviour). Unsigned alpha planes of up to 64 bits are now filled with their own sample width. For other kinds of alpha planes (signed, float, complex) no opaque value is defined and the decoded image carries a warning. (medium) - (GHSA-hqpw-h8p6-22q9) Heap out-of-bounds read of up to 6 bytes in the FFmpeg decoder plugin. The compressed data was passed to FFmpeg's parser without the
AV_INPUT_BUFFER_PADDING_SIZEbytes of padding that FFmpeg requires for its input buffers. This happened with ordinary files. The bytes were only read and not returned to the caller. The input is now padded, and the parser is no longer used (#1921). (low) - (GHSA-hvwh-xpj6-ph5x) Use after free of an error message in the WebP (since v1.22.1) and raw (since v1.22.0) input loaders of heif-enc. They released their image and returned its error, whose message heif-enc then printed. The lifetime of
heif_error.messageis now documented in the public header: the string belongs to the object on which the failing function was called and is only valid until the next call on that object, or until the object is released. (low) - (GHSA-v5rj-g4wv-j5w3) Decoded images could contain sample values above their declared bit depth. The
uncimixed-interleave decoder read each chroma sample with the width of the storage word instead of the bit depth of its component, so that the bits of the next sample or the padding bits ended up in the value. Well-formed files with a chroma bit depth other than 8 or 16 were decoded with wrong chroma. The OpenJPEG plugin stored the negative values of signed JPEG 2000 components into unsigned samples. Code that relies on the declared bit depth can be made to read out of bounds by such samples, as libsharpyuv was (GHSA-q7mw-2fmm-5q94). Mixed-interleave chroma is now read with its component bit depth, including alignment padding androw_align_size, and the OpenJPEG plugin refuses signed components. (low)
Thanks to @hackerman70000 and @john-preston for reporting these issues, and to @moo-fahmii and @overtimepog for the reports behind the unci row size and MIAF items under Hardening.
Hardening
- Encoding refuses an image whose samples exceed its declared bit depth, with a usage error that names the plane. With out-of-range 10-bit samples, x264 aborted on an internal assertion, OpenJPEG failed to encode, and the other encoders encoded the image without complaint
- The result of every colour conversion step is checked against the state that the operation declared: colorspace, chroma format, planes, bit depths and plane sizes. An operation that contradicts its declaration made the next one read planes that do not exist or that have another sample size. The RGB to YCbCr operation declines targets that are not planar YCbCr, and the alpha operations decline filter-array images
heif_image_add_plane()checks the bit depth of interleaved formats: 8 bits for interleaved RGB and RGBA, 9 to 16 bits for the RRGGBB formats. An interleaved plane with 64-bit or 128-bit components ran into an assertion, and without assertionsheif_image_get_bits_per_pixel()returned a truncated value- Overlay composition checks the plane layout of every input image. With colour planes that are larger than the image, the blend loop read past the end of the alpha plane (only reachable through the internal classes)
uncipixel interleave: the row size is computed in closed form instead of with a loop over all pixel columns (GHSA-vr5j-9r89-725x). The loop took about 0.02 s for a 16.7 million pixel wide row in a release build, and far longer in a sanitizer build- JPEG 2000: a codestream whose SIZ marker segment does not directly follow the SOC marker is not passed to a decoder anymore. OpenJPEG and FFmpeg skip data in between and would use a SIZ marker segment that libheif did not check
- The MIAF derivation constraints (ISO/IEC 23000-22, clause 7.3.11) are also checked for the auxiliary images of coded images. The alpha image of a plain coded primary image was never validated (#1929)
- The
sdtpbox bounds its sample count bymax_sequence_framesand charges its table to the memory limits, like the other sample tables - Box dumps are written into a single stream instead of copying the strings of all child boxes at every nesting level, and the sample tables print at most 100 entries. A 1 MB
sdtpbox nested inj2kHcontainers produced a 200 MB dump and timed out the box fuzzer (found by OSS-Fuzz) - A tile position outside of the tiling is also rejected when the image transformations are ignored. It was only checked as part of the conversion of the tile position
- A data extent that holds no data does not fall through to reading from a file that it does not have (null pointer dereference)
- HDR RGB to YCbCr 4:2:0 conversion: the row stride is no longer truncated to
int
Bug fixes
- Decoding single tiles (
heif_image_handle_decode_image_tile()) of images that are combined from several image items:- Regression since v1.22.0: decoding the single tile of an image with a
clapcrop, or with a rotation by 90 degrees of a non-square image, failed with "Decoded image does not have the size signaled in the file" - With a rotation or mirroring, the alpha channel of a tile came from a different tile, because the tile position was converted twice
- The tiles of a grid whose tiles have alpha images of their own (as heif-enc writes them) were returned without alpha channel
- An
idenimage over a tiled image decoded only the first tile, which was then rejected for its size
- Regression since v1.22.0: decoding the single tile of an image with a
- Uncompressed image sequences (
uncv) with tiles: all tiles of a frame showed the content of the first tile, and the sample was read from the file once for each tile - The bilinear 4:2:0 chroma upsampling took the Cb and Cr samples of the outermost rows and columns from the wrong position (#1930). This affected decoding with bilinear upsampling, decoding a 4:2:0 image to YCbCr 4:4:4, encoders that need 4:4:4 input (JPEG 2000), and 4:2:0 images with a crop at an odd position
- Overlay (
iovl) images with more than 8 bits per sample were composed byte by byte and decoded to a wrong picture. An alpha plane with another bit depth than the colour planes is weighted with its own range - A grid, overlay or
idenimage that comes before its input images in the file got no component descriptions (an overlay was described with 8 bits) - These functions crashed with a null pointer dereference when they were called on the handle returned by
heif_context_encode_image():heif_decode_image(),heif_image_handle_decode_image_tile(),heif_image_handle_get_luma_bits_per_pixel(),heif_image_handle_get_chroma_bits_per_pixel(),heif_image_handle_get_preferred_decoding_colorspace()andheif_image_handle_has_alpha_channel(). Decoding an image from the context it was encoded into is not supported; the functions now return an error, -1 or 0 - HEVC encoding: the 48 constraint flags in the
hvcCbox were always zero. The SPS parser skipped them, and the writer shifted each byte once too often. They now match the SPS, as ISO/IEC 14496-15 demands - HEVC encoding:
heixorhevxwas written for profiles that have no HEVC brand (high throughput and screen content coding profiles), andmiafwas written for every HEVC image, also outside of the MIAF profiles heif_context_write_to_file()always returned success. When the file could not be opened or written (missing directory, full disk), heif-enc ended without a message, without an output file and with exit code 0- heif-enc ignored unknown options and options without their argument, so a mistyped option was encoded with the default value
- heif-enc did not release the encoder when it was called without an input file, and neither the encoder nor the encoding options after a write error
- FFmpeg decoder plugin:
- HEVC and AVC streams with
matrix_coefficients0 (planar GBR, up to 14 bits for AVC) and 9-bit streams are decoded (#1920) - Full-range 8-bit 4:2:2 and 4:4:4 output (AVC, and every JPEG with this chroma format) is accepted
- A JPEG that is coded without colour transform is decoded with the right colours when the file has no
colrbox - Images with a left conformance window crop were rejected with "Decoded image does not have the size signaled in the file", because FFmpeg rounded the crop down (#1922)
- Complete access units are sent to the decoder without running the parser over them first (#1921)
- HEVC and AVC streams with
Behavior changes
- A decoded image whose bit depth contradicts the image handle is refused with "Decoded image does not have the bit depth signaled in the file"
- All tiles of a grid image need the same colorspace, chroma format and bit depths, and the same components. A grid in which, for example, only some of the tiles have an alpha channel is refused; whether its decoded image had an alpha plane depended on timing before.
heif_context_add_image_tile()andheif_context_encode_grid()do not check this yet when the tiles are added; the requirement is now documented - Overlay (
iovl) images are composed with the bit depth of their first input image instead of always 8 bits. An input image with more bits than the first one, and a canvas with more than 16 bits, are refused heif_image_tilingreports a single tile for an image whose alpha image has a different tiling, and for anunciimage whose compressed units span several tiles- A tile position outside of the tiling returns a usage error, also with
ignore_transformations unciwith generic compression: a compressed unit has to decompress to exactly the size that libheif computes for it. Surplus data was ignored and missing data was read as zeros before. This also refuses 4:2:0 and 4:2:2 images with an odd width (or an odd height for 4:2:0) that heif-enc wrote with compression; they were decoded with wrong chroma before- An
uncvsample that is smaller than the frame data is an error - Encoding an image with sample values above its declared bit depth returns a usage error
heif_image_add_plane()refuses bit depths other than 8 for interleaved RGB and RGBA (the legacy values 24 and 32 are still accepted) and other than 9 to 16 for the RRGGBB formats- HEVC files written by libheif carry the constraint flags of the SPS in
hvcC. Themiafbrand is not written for HEVC images with more than 10 bits or in a high throughput or screen content coding profile, and those two profile families get no HEVC brand - Encoding a 16-bit image as HEVC returns
heif_error_Unsupported_featurewithheif_suberror_Unsupported_bit_depthinstead of an encoder plugin error. ThehvcCbox cannot signal 16 bits - JPEG 2000: the OpenJPEG plugin refuses codestreams with signed components, codestreams that do not start with an SOC marker and an SIZ marker segment, and codestreams with more tiles or components than
max_number_of_tiles,max_componentsandmax_memory_block_sizeallow - An image sequence track with a chunk that references a non-visual sample entry is rejected when the file is read
- An HEVC SPS that cannot be parsed is an error instead of being skipped. The FFmpeg plugin refuses NAL units that contain
00 00 00,00 00 01or00 00 02 heif_context_write_to_file()returnsheif_error_Encoding_errorwithheif_suberror_Cannot_write_output_datawhen the file cannot be written- heif-enc exits with code 5 for an unknown option or an option without its argument. The option
--even-size, deprecated since v1.10.0 and without function since v1.19.0, is gone heif-info -dandheif_context_debug_dump_boxes_to_file()print at most 100 entries per sample table, followed by a note about how many were omitted- With bilinear chroma upsampling, the pixels in the outermost rows and columns of a 4:2:0 image change