Commit 40f361a1 for libheif
commit 40f361a1e1c3b8f1a90d4bace1d93227c06448de
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon May 25 12:03:55 2026 +0200
fix heif_region_item_add_region_inline_mask() when passed image size does not equal specified size (GHSA-5hqq-636x-r3cr)
diff --git a/libheif/api/libheif/heif_regions.cc b/libheif/api/libheif/heif_regions.cc
index cacdfdeb..8ea1104b 100644
--- a/libheif/api/libheif/heif_regions.cc
+++ b/libheif/api/libheif/heif_regions.cc
@@ -717,14 +717,20 @@ heif_error heif_region_item_add_region_inline_mask(heif_region_item* item,
uint32_t mask_width = mask_image->image->get_width();
size_t stride;
uint8_t* p = heif_image_get_plane2(mask_image, heif_channel_Y, &stride);
- uint64_t pixel_index = 0;
- for (uint32_t y = 0; y < mask_height; y++) {
- for (uint32_t x = 0; x < mask_width; x++) {
+ // The destination mask buffer is sized for the declared region (width x height).
+ // If the source image is larger than the region, crop it; if it is smaller, the
+ // remaining destination bits stay zero (zero-filled by the memset above).
+ // The destination bit index is computed from the declared 'width' so that each
+ // source row maps to the correct region row.
+ uint32_t copy_width = std::min(width, mask_width);
+ uint32_t copy_height = std::min(height, mask_height);
+
+ for (uint32_t y = 0; y < copy_height; y++) {
+ for (uint32_t x = 0; x < copy_width; x++) {
uint8_t mask_bit = p[y * stride + x] & 0x80; // use high-order bit of the 8-bit mask value as binary mask value
+ uint64_t pixel_index = static_cast<uint64_t>(y) * width + x;
region->mask_data.data()[pixel_index / 8] |= uint8_t(mask_bit >> (pixel_index % 8));
-
- pixel_index++;
}
}
diff --git a/tests/region.cc b/tests/region.cc
index f8e17ae5..db2bd1e0 100644
--- a/tests/region.cc
+++ b/tests/region.cc
@@ -715,3 +715,86 @@ TEST_CASE("create inline mask region from image") {
heif_image_handle_release(readbackHandle);
heif_context_free(readbackCtx);
}
+
+
+TEST_CASE("inline mask region from oversized image is cropped") {
+ // Regression test for GHSA-5hqq-636x-r3cr: when the source mask image is
+ // larger than the declared region, the old code sized the destination mask
+ // buffer from the region dimensions but indexed writes by the source image
+ // dimensions, causing a heap out-of-bounds write. Per the documented API
+ // contract the source image must be cropped to the region instead.
+ 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);
+
+ // Source mask image (64x64) much larger than the declared region (8x2).
+ // With the bug this writes ~4 KiB into a 2-byte buffer.
+ heif_image* mask_image;
+ heif_image_create(64, 64, heif_colorspace_monochrome, heif_chroma_monochrome, &mask_image);
+ err = heif_image_add_plane(mask_image, heif_channel_Y, 64, 64, 8);
+ REQUIRE(err.code == heif_error_Ok);
+ int stride;
+ uint8_t* p = heif_image_get_plane(mask_image, heif_channel_Y, &stride);
+ memset(p, 0, 64 * stride);
+ p[0] = 0x80; // region pixel (0,0) -> set
+ p[7] = 0x80; // region pixel (7,0) -> set
+ p[stride + 0] = 0x80; // region pixel (0,1) -> set
+ // Pixels outside the 8x2 region must be cropped away. With the old code these
+ // would corrupt the in-bounds result (p[10]) or write out of bounds (p[2*stride]):
+ p[10] = 0x80; // (10,0) beyond declared width
+ p[2 * stride + 0] = 0x80; // (0,2) beyond declared height
+
+ heif_region* out_region = nullptr;
+ err = heif_region_item_add_region_inline_mask(region_item, 20, 50, 8, 2, mask_image, &out_region);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(out_region != nullptr);
+
+ size_t data_len = heif_region_get_inline_mask_data_len(out_region);
+ REQUIRE(data_len == 2); // (8*2+7)/8 = 2 bytes
+
+ std::vector<uint8_t> mask_data_in(data_len);
+ int32_t x, y;
+ uint32_t width, height;
+ err = heif_region_get_inline_mask_data(out_region, &x, &y, &width, &height, mask_data_in.data());
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(x == 20);
+ REQUIRE(y == 50);
+ REQUIRE(width == 8);
+ REQUIRE(height == 2);
+ // Destination bits, row-major over the 8x2 region:
+ // (0,0)=index0 -> 0x80, (7,0)=index7 -> 0x01 => byte0 = 0x81
+ // (0,1)=index8 -> 0x80 => byte1 = 0x80
+ // The cropped-away pixels (10,0) and (0,2) must not appear. The old code
+ // would instead set byte1 = 0x20 (from the source pixel at x=10) and write
+ // the region's second row out of bounds.
+ REQUIRE(mask_data_in[0] == 0x81);
+ REQUIRE(mask_data_in[1] == 0x80);
+
+ heif_region_release(out_region);
+ heif_image_release(mask_image);
+ heif_region_item_release(region_item);
+ heif_image_handle_release(handle);
+ heif_encoder_release(enc);
+ heif_context_free(ctx);
+ heif_image_release(img);
+}