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);
+}