Commit 646d85fb for libheif
commit 646d85fbf5bd18fc3cdc516e915e59072de2d510
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon Aug 24 22:30:44 2026 +0200
validate inline mask data length in region writer API (GHSA-p58j-h3vm-3fp5)
heif_region_item_add_region_inline_mask_data() stored the caller-supplied
buffer using mask_data_len verbatim, without checking it against the region
geometry. The reader (heif_region_get_inline_mask_image) derives the number
of mask bytes from width*height, so an undersized buffer caused a heap
out-of-bounds read whose bytes were also copied into the returned mask image,
potentially disclosing adjacent heap memory. Not reachable through file
parsing: RegionGeometry_InlineMask::parse already validates the input.
The writer now rejects a NULL buffer, zero width/height, and any mask_data_len
that does not equal the canonical (width*height+7)/8 bytes. An oversized buffer
is rejected as well, since a surplus indicates that geometry and data disagree
and the extra bytes would desynchronize on-disk parsing of following regions.
As defence in depth, heif_region_get_inline_mask_image() clamps its reads to
the actual buffer size.
diff --git a/libheif/api/libheif/heif_regions.cc b/libheif/api/libheif/heif_regions.cc
index 8ea1104b..6ae402c4 100644
--- a/libheif/api/libheif/heif_regions.cc
+++ b/libheif/api/libheif/heif_regions.cc
@@ -409,12 +409,43 @@ heif_error heif_region_item_add_region_inline_mask_data(heif_region_item* item,
size_t mask_data_len,
heif_region** out_region)
{
+ if (out_region) {
+ *out_region = nullptr;
+ }
+
+ 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 region has zero width or height"};
+ }
+
+ // The mask is stored with one bit per pixel, no padding between rows. Only the
+ // very last byte is padded. This is the same canonical size that the file parser
+ // (RegionGeometry_InlineMask::parse) and the reader (heif_region_get_mask_image)
+ // compute from the geometry. The caller-supplied buffer must hold exactly that
+ // many bytes: a shorter buffer would make the reader run off the end of the
+ // allocation, and a longer buffer indicates that the caller's geometry and mask
+ // data disagree, which we reject rather than silently discard.
+ 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 length does not match the given region size"};
+ }
+
auto region = std::make_shared<RegionGeometry_InlineMask>();
region->x = x;
region->y = y;
region->width = width;
region->height = height;
- region->mask_data.resize(mask_data_len);
+ region->mask_data.resize(static_cast<size_t>(mask_size));
std::memcpy(region->mask_data.data(), mask_data, region->mask_data.size());
item->region_item->add_region(region);
@@ -442,7 +473,11 @@ static heif_error heif_region_get_inline_mask_image(const heif_region* region,
*out_y0 = mask->y;
uint32_t width = *out_width = mask->width;
uint32_t height = *out_height = mask->height;
- uint8_t* mask_data = mask->mask_data.data();
+ const uint8_t* mask_data = mask->mask_data.data();
+ // Defence in depth: the writer API and the file parser both guarantee that
+ // mask_data holds ceil(width*height/8) bytes, but never read beyond the actual
+ // buffer even if that invariant were somehow violated.
+ size_t mask_data_len = mask->mask_data.size();
heif_error err = heif_image_create(width, height, heif_colorspace_monochrome, heif_chroma_monochrome, out_mask_image);
if (err.code) {
@@ -462,7 +497,7 @@ static heif_error heif_region_get_inline_mask_image(const heif_region* region,
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;
+ p[y * stride + x] = (mask_byte < mask_data_len && (mask_data[mask_byte] & pixel_bit)) ? 255 : 0;
pixel_index++;
}
diff --git a/libheif/api/libheif/heif_regions.h b/libheif/api/libheif/heif_regions.h
index d9a3f61f..eaa9c170 100644
--- a/libheif/api/libheif/heif_regions.h
+++ b/libheif/api/libheif/heif_regions.h
@@ -814,12 +814,16 @@ heif_error heif_region_item_add_region_referenced_mask(heif_region_item* region_
* part of the region. If the bit value is `0`, the corresponding pixel is not part of the
* region.
*
+ * @c width and @c height must both be non-zero and @c mask_data_len must be exactly
+ * `(width * height + 7) / 8`, the number of bytes needed to hold one bit per pixel.
+ * Otherwise the function fails without modifying the region item.
+ *
* @param region_item the region item that holds this mask region
* @param x the x value for the top-left corner of this mask region
* @param y the y value for the top-left corner of this mask region
* @param width the width of this mask region
* @param height the height of this mask region
- * @param mask_data the location to return the mask data
+ * @param mask_data the mask data to store
* @param mask_data_len the length of the mask data, in bytes
* @param out_region pointer to pointer to the returned region (optional, see below)
* @return heif_error_ok on success, or an error value indicating the problem on failure
diff --git a/tests/region.cc b/tests/region.cc
index db2bd1e0..e32bd952 100644
--- a/tests/region.cc
+++ b/tests/region.cc
@@ -798,3 +798,179 @@ TEST_CASE("inline mask region from oversized image is cropped") {
heif_context_free(ctx);
heif_image_release(img);
}
+
+
+TEST_CASE("inline mask region data length must match region geometry") {
+ // Regression test for GHSA-p58j-h3vm-3fp5: heif_region_item_add_region_inline_mask_data()
+ // stored the caller-supplied buffer verbatim without checking that it holds at least
+ // (width*height+7)/8 bytes. heif_region_get_mask_image() derives the number of mask
+ // bytes to read from the region geometry, so an undersized buffer led to a heap
+ // out-of-bounds read whose bytes were copied into the returned mask image.
+ struct heif_error err;
+
+ heif_context* ctx = heif_context_alloc();
+ heif_encoder* enc;
+ err = heif_context_get_encoder_for_format(ctx, heif_compression_AV1, &enc);
+ REQUIRE(err.code == heif_error_Ok);
+
+ uint32_t input_width = 64;
+ uint32_t input_height = 64;
+ heif_image* img;
+ heif_image_create(input_width, input_height, heif_colorspace_YCbCr,
+ heif_chroma_420, &img);
+ fill_new_plane(img, heif_channel_Y, input_width, input_height);
+ fill_new_plane(img, heif_channel_Cb, (input_width + 1) / 2, (input_height + 1) / 2);
+ fill_new_plane(img, heif_channel_Cr, (input_width + 1) / 2, (input_height + 1) / 2);
+
+ heif_image_handle* handle;
+ err = heif_context_encode_image(ctx, img, enc, nullptr, &handle);
+ REQUIRE(err.code == heif_error_Ok);
+
+ struct heif_region_item* region_item;
+ err = heif_image_handle_add_region_item(handle, input_width, input_height, ®ion_item);
+ REQUIRE(err.code == heif_error_Ok);
+
+ // A 64x3 region needs (64*3+7)/8 = 24 bytes of mask data.
+ const uint32_t region_width = 64;
+ const uint32_t region_height = 3;
+ const size_t required_len = (region_width * region_height + 7) / 8;
+ REQUIRE(required_len == 24);
+
+ heif_region* out_region = nullptr;
+
+ // Only 1 byte instead of 24. With the bug this was accepted and rendering the
+ // mask afterwards read 23 bytes past the end of the 1-byte allocation.
+ std::vector<uint8_t> short_mask_data(1, 0xff);
+ err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, region_width, region_height,
+ short_mask_data.data(), short_mask_data.size(),
+ &out_region);
+ REQUIRE(err.code != heif_error_Ok);
+ REQUIRE(out_region == nullptr);
+
+ // Off by one (23 of 24 bytes) must be rejected as well.
+ std::vector<uint8_t> almost_mask_data(required_len - 1, 0xff);
+ err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, region_width, region_height,
+ almost_mask_data.data(), almost_mask_data.size(),
+ &out_region);
+ REQUIRE(err.code != heif_error_Ok);
+ REQUIRE(out_region == nullptr);
+
+ // The file parser rejects zero-sized inline masks, so the writer must not produce them.
+ std::vector<uint8_t> mask_data(required_len, 0x00);
+ err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, 0, region_height,
+ mask_data.data(), mask_data.size(), &out_region);
+ REQUIRE(err.code != heif_error_Ok);
+ REQUIRE(out_region == nullptr);
+ err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, region_width, 0,
+ mask_data.data(), mask_data.size(), &out_region);
+ REQUIRE(err.code != heif_error_Ok);
+ REQUIRE(out_region == nullptr);
+
+ // NULL mask data.
+ err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, region_width, region_height,
+ nullptr, required_len, &out_region);
+ REQUIRE(err.code != heif_error_Ok);
+ REQUIRE(out_region == nullptr);
+
+ // None of the rejected calls may have added a region.
+ REQUIRE(heif_region_item_get_number_of_regions(region_item) == 0);
+
+ // A buffer of exactly the required size is accepted and can be rendered as a mask image.
+ mask_data[0] = 0x80; // pixel (0,0)
+ mask_data[23] = 0x01; // pixel (63,2), the very last pixel of the region
+ err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, region_width, region_height,
+ mask_data.data(), mask_data.size(), &out_region);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(out_region != nullptr);
+ REQUIRE(heif_region_get_inline_mask_data_len(out_region) == required_len);
+
+ int32_t x, y;
+ uint32_t width, height;
+ heif_image* mask_image = nullptr;
+ err = heif_region_get_mask_image(out_region, &x, &y, &width, &height, &mask_image);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(mask_image != nullptr);
+ REQUIRE(x == 20);
+ REQUIRE(y == 50);
+ REQUIRE(width == 64);
+ REQUIRE(height == 3);
+ int stride;
+ const uint8_t* p = heif_image_get_plane_readonly(mask_image, heif_channel_Y, &stride);
+ REQUIRE(p[0] == 0xff);
+ REQUIRE(p[1] == 0x00);
+ REQUIRE(p[2 * stride + 62] == 0x00);
+ REQUIRE(p[2 * stride + 63] == 0xff);
+ heif_image_release(mask_image);
+ heif_region_release(out_region);
+ out_region = nullptr;
+
+ // A buffer larger than required is rejected as well: a surplus indicates that the
+ // caller's geometry and mask data disagree. (Silently keeping only the canonical
+ // prefix would also risk desynchronizing the on-disk parsing of following regions,
+ // since the format stores exactly (width*height+7)/8 bytes.)
+ std::vector<uint8_t> long_mask_data(required_len + 8, 0xaa);
+ err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, region_width, region_height,
+ long_mask_data.data(), long_mask_data.size(),
+ &out_region);
+ REQUIRE(err.code != heif_error_Ok);
+ REQUIRE(out_region == nullptr);
+
+ // Add a second, correctly-sized inline mask so the round-trip below exercises
+ // parsing of multiple consecutive inline-mask regions.
+ std::vector<uint8_t> mask_data2(required_len, 0xaa);
+ err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, region_width, region_height,
+ mask_data2.data(), mask_data2.size(), &out_region);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(out_region != nullptr);
+ REQUIRE(heif_region_get_inline_mask_data_len(out_region) == required_len);
+ heif_region_release(out_region);
+
+ err = heif_region_item_add_region_point(region_item, 5, 7, nullptr);
+ REQUIRE(err.code == heif_error_Ok);
+
+ err = heif_context_write_to_file(ctx, "regions_mask_inline_data_lengths.heif");
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_region_item_release(region_item);
+ heif_image_handle_release(handle);
+ heif_encoder_release(enc);
+ heif_context_free(ctx);
+ heif_image_release(img);
+
+ // Read the file back: all three regions must still be parsable and intact.
+ heif_context* readbackCtx = get_context_for_local_file("regions_mask_inline_data_lengths.heif");
+ heif_image_handle* readbackHandle = get_primary_image_handle(readbackCtx);
+ REQUIRE(heif_image_handle_get_number_of_region_items(readbackHandle) == 1);
+ heif_item_id region_item_id;
+ REQUIRE(heif_image_handle_get_list_of_region_item_ids(readbackHandle, ®ion_item_id, 1) == 1);
+ heif_region_item* in_region_item;
+ err = heif_context_get_region_item(readbackCtx, region_item_id, &in_region_item);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(heif_region_item_get_number_of_regions(in_region_item) == 3);
+ heif_region* regions[3];
+ REQUIRE(heif_region_item_get_list_of_regions(in_region_item, regions, 3) == 3);
+ REQUIRE(heif_region_get_type(regions[0]) == heif_region_type_inline_mask);
+ REQUIRE(heif_region_get_type(regions[1]) == heif_region_type_inline_mask);
+ REQUIRE(heif_region_get_type(regions[2]) == heif_region_type_point);
+ REQUIRE(heif_region_get_inline_mask_data_len(regions[0]) == required_len);
+ REQUIRE(heif_region_get_inline_mask_data_len(regions[1]) == required_len);
+
+ std::vector<uint8_t> mask_data_in(required_len);
+ err = heif_region_get_inline_mask_data(regions[1], &x, &y, &width, &height, mask_data_in.data());
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(width == 64);
+ REQUIRE(height == 3);
+ REQUIRE(mask_data_in[0] == 0xaa);
+ REQUIRE(mask_data_in[23] == 0xaa);
+
+ int32_t px, py;
+ err = heif_region_get_point(regions[2], &px, &py);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(px == 5);
+ REQUIRE(py == 7);
+
+ heif_region_release_many(regions, 3);
+ heif_region_item_release(in_region_item);
+ heif_image_handle_release(readbackHandle);
+ heif_context_free(readbackCtx);
+}