Commit 4b12fc8d for libheif

commit 4b12fc8d6e0ee8b52f100ade37d2f179ce821aaa
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Sun Oct 25 17:51:31 2026 +0100

    Skip decode jobs for missing grid tiles [GHSA-rcqc-842v-m26h]

diff --git a/libheif/image-items/grid.cc b/libheif/image-items/grid.cc
index b8448089..1f0ecc54 100644
--- a/libheif/image-items/grid.cc
+++ b/libheif/image-items/grid.cc
@@ -26,6 +26,7 @@
 #include <deque>
 #include <future>
 #include <mutex>
+#include <optional>
 #include <set>
 #include <algorithm>
 #include <utility>
@@ -288,7 +289,9 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem_Grid::decode_full_grid_image(c
   // remember which tile to put where into the image
   struct tile_data
   {
-    heif_item_id tileID;
+    // An empty entry represents a tile skipped during non-strict decoding.
+    // Item ID 0 is not an empty entry.
+    std::optional<heif_item_id> tileID;
     uint32_t x_origin, y_origin;
   };

@@ -435,9 +438,15 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem_Grid::decode_full_grid_image(c
       tile_data data = tiles.front();
       tiles.pop_front();

+      // Skipped tiles leave empty queue entries. Do not decode an unrelated
+      // item (in particular item 0) or start a job for such an entry.
+      if (!data.tileID) {
+        continue;
+      }
+
       errs.push_back(std::async(std::launch::async,
                                 &ImageItem_Grid::decode_and_paste_tile_image, this,
-                                data.tileID, data.x_origin, data.y_origin, std::ref(img), options,
+                                *data.tileID, data.x_origin, data.y_origin, std::ref(img), options,
                                 std::ref(progress_counter), warnings, decode_state));
     }

diff --git a/tests/parallel_grid_deadlock.cc b/tests/parallel_grid_deadlock.cc
index 88062584..2eadaacd 100644
--- a/tests/parallel_grid_deadlock.cc
+++ b/tests/parallel_grid_deadlock.cc
@@ -398,6 +398,82 @@ TEST_CASE("parallel grid: a normal two-tile grid still decodes") {
   REQUIRE(err.code == heif_error_Ok);
 }

+// A skipped tile must not turn its default queue entry into a decode of item 0.
+// Use an unrelated item with different pixels so the erroneous decode is
+// visible without making the regression test risk a permanent deadlock.
+TEST_CASE("parallel grid: a skipped tile does not decode item 0") {
+  const std::vector<uint8_t> tile_pixels(32 * 32, 0x7F);
+  const std::vector<uint8_t> unrelated_pixels(32 * 32, 0xFF);
+  std::vector<uint8_t> unsupported_grid = image_grid(1, 1, 32, 32);
+  unsupported_grid[0] = 1;  // unsupported grid version
+
+  std::vector<Item> items;
+  items.push_back({1, "grid", 64, 32, false, {2, 3}, {}, image_grid(1, 2, 64, 32)});
+  items.push_back({2, "mski", 32, 32, false, {},     {}, tile_pixels});
+  items.push_back({3, "grid", 32, 32, false, {},     {}, unsupported_grid});
+  items.push_back({0, "mski", 32, 32, false, {},     {}, unrelated_pixels});
+
+  auto data = build_file(items, /*primary_id=*/1, /*with_miaf_brand=*/false);
+  heif_context* ctx = heif_context_alloc();
+  heif_context_set_max_decoding_threads(ctx, 1);
+
+  heif_error err = heif_context_read_from_memory_without_copy(ctx, data.data(), data.size(), nullptr);
+  REQUIRE(err.code == heif_error_Ok);
+
+  heif_image_handle* handle = nullptr;
+  err = heif_context_get_primary_image_handle(ctx, &handle);
+  REQUIRE(err.code == heif_error_Ok);
+
+  heif_image* img = nullptr;
+  err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr);
+  REQUIRE(err.code == heif_error_Ok);
+
+  size_t stride = 0;
+  const uint8_t* pixels = heif_image_get_plane_readonly2(img, heif_channel_Y, &stride);
+  REQUIRE(pixels != nullptr);
+  REQUIRE(pixels[0] == 0x7F);
+  REQUIRE(heif_image_get_decoding_warnings(img, 0, nullptr, 0) == 1);
+
+  heif_image_release(img);
+  heif_image_handle_release(handle);
+  heif_context_free(ctx);
+}
+
+// An explicitly referenced tile with ID 0 must remain distinguishable from an empty job.
+TEST_CASE("parallel grid: an explicitly referenced item 0 is decoded") {
+  const std::vector<uint8_t> zero_tile_pixels(32 * 32, 0x5A);
+  const std::vector<uint8_t> other_tile_pixels(32 * 32, 0x7F);
+  std::vector<Item> items;
+  items.push_back({1, "grid", 64, 32, false, {0, 2}, {}, image_grid(1, 2, 64, 32)});
+  items.push_back({0, "mski", 32, 32, false, {},     {}, zero_tile_pixels});
+  items.push_back({2, "mski", 32, 32, false, {},     {}, other_tile_pixels});
+
+  auto data = build_file(items, /*primary_id=*/1, /*with_miaf_brand=*/false);
+  heif_context* ctx = heif_context_alloc();
+  heif_context_set_max_decoding_threads(ctx, 1);
+
+  heif_error err = heif_context_read_from_memory_without_copy(ctx, data.data(), data.size(), nullptr);
+  REQUIRE(err.code == heif_error_Ok);
+
+  heif_image_handle* handle = nullptr;
+  err = heif_context_get_primary_image_handle(ctx, &handle);
+  REQUIRE(err.code == heif_error_Ok);
+
+  heif_image* img = nullptr;
+  err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr);
+  REQUIRE(err.code == heif_error_Ok);
+
+  size_t stride = 0;
+  const uint8_t* pixels = heif_image_get_plane_readonly2(img, heif_channel_Y, &stride);
+  REQUIRE(pixels != nullptr);
+  REQUIRE(pixels[0] == 0x5A);
+  REQUIRE(pixels[32] == 0x7F);
+
+  heif_image_release(img);
+  heif_image_handle_release(handle);
+  heif_context_free(ctx);
+}
+
 // A reference cycle is not rejected at file load: the file still opens and its
 // independently valid items still decode. Only the cyclic item itself fails,
 // at decode time. Here the primary (item 1) is a grid whose two sub-grids form