Commit c1f6abf9 for libheif

commit c1f6abf9b8d9f8a27178a0f2084754f24ef6c7c1
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Sat Sep 5 14:57:40 2026 +0200

    Enforce MIAF derived-image dependency constraints in the decode validator

    Extend ImageItem::verify_decodable() with an optional check of MIAF's derivation
    chain (ISO/IEC 23000-22, clause 7.3.11). The chain, from base to top, is:
    coded image -> [iden] -> grid -> [iden] -> overlay -> [iden]; an 'iden' may not
    derive directly from another 'iden' (7.3.11.2); and a grid tile that is an
    'iden' must refer directly to a coded image (7.3.11.4.1). This is modeled with a
    structural rank (coded=0, grid=1, overlay=2): a derived item must not exceed the
    rank its position allows, and it lowers the rank permitted for its own inputs.
    The walk memoizes (item, context) triples so shared sub-images stay linear.

    The check is applied when the file declares the 'miaf' brand. (A future
    security-limits flag, always_apply_MIAF_derivation_constraints, will be able to
    force it on so a malicious file cannot bypass it by omitting the brand; that is
    an API addition and has to wait for v1.24.x.)

    Also harden the cycle walk added for GHSA-prgh-72vc-3xmc: an input item that
    failed to parse is a detached error item with a null context, so resolving its
    file/references would dereference null. Both walks now treat such an item as a
    leaf. (Reproduced by a grid whose tile item did not parse.)

    Adds regression tests: a nested grid (grid input that is itself a grid) is
    rejected with the 'miaf' brand and accepted without it.

diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index 34590736..f6a1b2a3 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -912,6 +912,14 @@ Error check_decode_reference_cycles(const ImageItem* item,
     return Error::Ok;
   }

+  // An item that failed to parse is a detached error item with no context and
+  // no traversable references. It cannot be part of a cycle, so treat it as a
+  // leaf (and, crucially, do not dereference its null context via get_file()).
+  if (item->get_context() == nullptr) {
+    verified.insert(id);
+    return Error::Ok;
+  }
+
   on_path.insert(id);

   // Follow exactly the edges the decode recursion follows: the derived-image
@@ -941,14 +949,153 @@ Error check_decode_reference_cycles(const ImageItem* item,
   return Error::Ok;
 }

+
+// --- MIAF derived-image dependency constraints (ISO/IEC 23000-22, clause 7.3.11)
+//
+// MIAF restricts the derivation chain to a fixed order. From base to top it is:
+//   coded image(s) -> [iden] -> grid -> [iden] -> overlay -> [iden]
+// (7.3.11.1), plus: an 'iden' shall not be derived directly from another 'iden'
+// (7.3.11.2), and a grid tile that is an 'iden' must refer directly to a coded
+// image (7.3.11.4.1). So, ignoring 'iden', a chain may apply overlay above grid
+// above the coded base, each at most once. We model that with a "structural
+// rank": coded=0, grid=1, overlay=2. Walking from the top down, each derived
+// item must have rank <= the rank its position allows, and it lowers the rank
+// allowed for its own inputs (grid inputs must be coded; overlay inputs may be
+// grid or below). 'iden' is transparent to the rank but must not sit directly
+// on another 'iden'. Only the 'dimg' derivation is constrained here; auxiliary
+// images are checked as their own fresh chains.
+enum { MIAF_RANK_CODED = 0, MIAF_RANK_GRID = 1, MIAF_RANK_OVERLAY = 2 };
+
+int miaf_structural_rank(const ImageItem* item, bool& is_iden)
+{
+  uint32_t type = item->get_infe_type();
+  is_iden = (type == fourcc("iden"));
+  if (type == fourcc("iovl")) { return MIAF_RANK_OVERLAY; }
+  if (type == fourcc("grid")) { return MIAF_RANK_GRID; }
+  return MIAF_RANK_CODED;  // coded image, or 'iden' (rank unused when is_iden)
+}
+
+// `max_rank` is the highest structural rank allowed at this item's position;
+// `parent_is_iden` is true when the immediate parent on the derivation path is
+// an 'iden'. `verified` memoizes (item, max_rank, parent_is_iden) triples that
+// already passed, keeping a shared sub-image from being re-walked per path.
+Error check_miaf_derivation_constraints(const ImageItem* item,
+                                        int max_rank, bool parent_is_iden,
+                                        std::set<uint64_t>& verified)
+{
+  heif_item_id id = item->get_id();
+
+  bool is_iden = false;
+  int rank = miaf_structural_rank(item, is_iden);
+
+  if (is_iden) {
+    if (parent_is_iden) {
+      return {heif_error_Invalid_input, heif_suberror_Unspecified,
+              "MIAF: an 'iden' image is derived directly from another 'iden' image"};
+    }
+  }
+  else if (rank > max_rank) {
+    return {heif_error_Invalid_input, heif_suberror_Unspecified,
+            "MIAF: derived-image dependencies are not in the order allowed by ISO/IEC 23000-22"};
+  }
+
+  uint64_t key = (static_cast<uint64_t>(id) << 4) |
+                 (static_cast<uint64_t>(max_rank & 0x3) << 2) |
+                 (parent_is_iden ? 2u : 0u) | (is_iden ? 1u : 0u);
+  if (!verified.insert(key).second) {
+    return Error::Ok;  // already verified in this context
+  }
+
+  // A failed-to-parse item is a detached error item (null context) with no
+  // traversable references; treat it as a leaf and do not dereference it.
+  if (item->get_context() == nullptr) {
+    return Error::Ok;
+  }
+
+  // Rank budget passed to this item's own 'dimg' inputs.
+  int child_max_rank;
+  bool child_parent_is_iden;
+  if (is_iden) {
+    child_max_rank = max_rank;          // transparent: inputs keep this position
+    child_parent_is_iden = true;
+  }
+  else if (rank == MIAF_RANK_OVERLAY) {
+    child_max_rank = MIAF_RANK_GRID;    // overlay inputs: grid or below
+    child_parent_is_iden = false;
+  }
+  else if (rank == MIAF_RANK_GRID) {
+    child_max_rank = MIAF_RANK_CODED;   // grid inputs: coded (or iden -> coded)
+    child_parent_is_iden = false;
+  }
+  else {
+    return Error::Ok;                   // coded image: leaf of the derivation chain
+  }
+
+  auto file = item->get_file();
+  auto iref = file ? file->get_iref_box() : nullptr;
+  if (iref) {
+    for (heif_item_id child_id : iref->get_references(id, fourcc("dimg"))) {
+      auto child = item->get_context()->get_image(child_id, true);
+      if (child) {
+        if (Error err = check_miaf_derivation_constraints(child.get(), child_max_rank,
+                                                          child_parent_is_iden, verified)) {
+          return err;
+        }
+      }
+    }
+  }
+
+  // An auxiliary (e.g. alpha) image is a separate image whose own derivation
+  // chain must independently satisfy MIAF, so check it as a fresh chain.
+  if (auto alpha = item->get_alpha_channel()) {
+    if (Error err = check_miaf_derivation_constraints(alpha.get(), MIAF_RANK_OVERLAY,
+                                                      /*parent_is_iden=*/false, verified)) {
+      return err;
+    }
+  }
+
+  return Error::Ok;
+}
+
 } // namespace


 Error ImageItem::verify_decodable() const
 {
-  std::set<heif_item_id> on_path;
-  std::set<heif_item_id> verified;
-  return check_decode_reference_cycles(this, on_path, verified);
+  // Always: reject a cyclic decode reference graph (both 'dimg' and 'auxl'
+  // edges) before decoding. See the declaration in image_item.h.
+  {
+    std::set<heif_item_id> on_path;
+    std::set<heif_item_id> verified;
+    if (Error err = check_decode_reference_cycles(this, on_path, verified)) {
+      return err;
+    }
+  }
+
+  // Optionally: enforce MIAF's restricted derived-image dependencies
+  // (ISO/IEC 23000-22, clause 7.3.11). Applied when the file declares the 'miaf'
+  // brand.
+  //
+  // TODO(v1.24.x): also apply this when a security-limits flag
+  // (always_apply_MIAF_derivation_constraints) is set, so that a malicious file
+  // cannot bypass the check simply by omitting the 'miaf' brand. That flag is a
+  // heif_security_limits API addition and therefore has to wait for v1.24.x.
+  bool apply_miaf = false;
+  if (auto file = get_file()) {
+    if (auto ftyp = file->get_ftyp_box()) {
+      apply_miaf = ftyp->has_compatible_brand(heif_brand2_miaf);
+    }
+  }
+
+  if (apply_miaf) {
+    std::set<uint64_t> verified;
+    if (Error err = check_miaf_derivation_constraints(this, MIAF_RANK_OVERLAY,
+                                                      /*parent_is_iden=*/false, verified)) {
+      return err;
+    }
+  }
+
+  return Error::Ok;
 }


diff --git a/tests/parallel_grid_deadlock.cc b/tests/parallel_grid_deadlock.cc
index cf78397c..ec51b515 100644
--- a/tests/parallel_grid_deadlock.cc
+++ b/tests/parallel_grid_deadlock.cc
@@ -92,13 +92,16 @@ std::vector<uint8_t> image_grid(uint8_t rows, uint8_t cols, uint16_t w, uint16_t
 // (property index = item position, 1-based); the shared 'mskC' and 'auxC'
 // follow. Item payloads are stored in 'idat' and located with construction
 // method 1.
-std::vector<uint8_t> build_file(const std::vector<Item>& items, uint16_t primary_id) {
+std::vector<uint8_t> build_file(const std::vector<Item>& items, uint16_t primary_id,
+                                bool with_miaf_brand = true) {
   std::vector<uint8_t> ftyp_payload;
   append_fourcc(ftyp_payload, "heic");
   put_u32_be(ftyp_payload, 0);
   append_fourcc(ftyp_payload, "mif1");
   append_fourcc(ftyp_payload, "heic");
-  append_fourcc(ftyp_payload, "miaf");
+  if (with_miaf_brand) {
+    append_fourcc(ftyp_payload, "miaf");
+  }
   auto ftyp = make_box("ftyp", ftyp_payload);

   std::vector<uint8_t> hdlr_payload;
@@ -424,3 +427,42 @@ TEST_CASE("parallel grid: a cyclic primary does not make valid sibling items und
   REQUIRE(completed_cyclic);
   REQUIRE(err_cyclic.code == heif_error_Invalid_input);
 }
+
+// A nested grid (a grid whose tile is itself a grid) violates MIAF's derivation
+// chain (ISO/IEC 23000-22 clause 7.3.11: a grid input must be a coded image).
+// The graph is acyclic, so the cycle check passes; verify_decodable() rejects it
+// only because of the MIAF derivation constraints, which apply here because the
+// file carries the 'miaf' brand.
+static std::vector<Item> nested_grid_items() {
+  const std::vector<uint8_t> pixels(64 * 64, 0x7F);
+  std::vector<Item> items;
+  //           id  type    w   h   alpha dimg auxl data
+  items.push_back({1, "grid", 64, 64, false, {2}, {}, image_grid(1, 1, 64, 64)});  // grid over ...
+  items.push_back({2, "grid", 64, 64, false, {3}, {}, image_grid(1, 1, 64, 64)});  // ... a grid (invalid)
+  items.push_back({3, "mski", 64, 64, false, {},  {}, pixels});                     // coded base
+  return items;
+}
+
+TEST_CASE("MIAF: a nested grid is rejected when the file has the 'miaf' brand") {
+  auto data = build_file(nested_grid_items(), /*primary=*/1, /*with_miaf_brand=*/true);
+
+  heif_error err{};
+  bool completed = decode_item_with_timeout(data, /*item=*/1, std::chrono::seconds(20), err);
+
+  REQUIRE(completed);
+  REQUIRE(err.code == heif_error_Invalid_input);
+}
+
+// The same nested grid, without the 'miaf' brand, is not subject to the MIAF
+// derivation constraints. It is acyclic and structurally decodable, so it is
+// not rejected by verify_decodable(). (This documents that the MIAF check is
+// brand-gated; a future security-limits flag will be able to force it on.)
+TEST_CASE("MIAF: without the 'miaf' brand a nested grid is not structurally rejected") {
+  auto data = build_file(nested_grid_items(), /*primary=*/1, /*with_miaf_brand=*/false);
+
+  heif_error err{};
+  bool completed = decode_item_with_timeout(data, /*item=*/1, std::chrono::seconds(20), err);
+
+  REQUIRE(completed);
+  REQUIRE(err.code == heif_error_Ok);
+}