Commit 2ebb06f2 for libheif

commit 2ebb06f2b60f4542e193ae273988b728ba7c9083
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Sat Sep 5 00:40:59 2026 +0200

    Enforce max_items security limit on iinf child boxes (GHSA-vg7w-rp49-4fc2)

    Box_iinf::parse passed the file-declared item_count to read_children as a
    concrete child count, but the cardinality check in read_children only ran
    in the READ_CHILDREN_ALL branch. The limit was therefore never enforced
    for iinf, so a crafted file could declare an unbounded number of items,
    costing quadratic parsing time (via the per-item iref reference scan) and
    large amounts of heap that max_total_memory does not track.

    - Box_iinf::parse rejects item_count > max_items up front, mirroring
      Box_iloc::parse.
    - read_children hoists the child-count limit out of the READ_CHILDREN_ALL
      branch and rejects an excessive declared count directly. This also
      covers the other concrete-count callers (dref, tili).
    - Reject a declared count larger than the remaining input could hold
      (minimum box size is 8 bytes), which also bounds the new reserve() when
      security limits are disabled.
    - Add a post-loop check that the number of children read matches the
      declared count, catching clean-boundary short reads that EOF does not
      report as an error.

    Fixes GHSA-vg7w-rp49-4fc2.

diff --git a/libheif/box.cc b/libheif/box.cc
index d752c7d4..3ea69bf7 100644
--- a/libheif/box.cc
+++ b/libheif/box.cc
@@ -1107,7 +1107,52 @@ bool Box::equal(const std::shared_ptr<Box>& box1, const std::shared_ptr<Box>& bo

 Error Box::read_children(BitstreamRange& range, uint32_t max_number, const heif_security_limits* limits)
 {
-  uint32_t count = 0;
+  // A freshly parsed box has no children yet, so m_children.size() below tracks
+  // the number of children read so far.
+  assert(m_children.empty());
+
+  // Determine the applicable limit on the number of child boxes.
+  uint32_t max_children;
+  if (get_short_type() == fourcc("iinf")) {
+    max_children = limits->max_items;
+  }
+  else {
+    max_children = limits->max_children_per_box;
+  }
+
+  // If the number of children is known in advance, reject an excessive count
+  // directly instead of reading children until the accumulated count reaches
+  // the limit.
+  if (max_number != READ_CHILDREN_ALL) {
+    if (max_children && max_number > max_children) {
+      std::stringstream sstr;
+      sstr << "Number of child boxes (" << max_number << ") in '" << get_type_string()
+           << "' box exceeds the security limit of " << max_children << ".";
+
+      return Error(heif_error_Memory_allocation_error,
+                   heif_suberror_Security_limit_exceeded,
+                   sstr.str());
+    }
+
+    // The declared number of children cannot exceed what the remaining input
+    // could possibly hold (each box occupies at least an 8-byte header). A
+    // larger count means the box is truncated or malformed, so reject it
+    // before reading anything. This also bounds the reservation below, which
+    // matters when security limits are disabled and 'max_number' is otherwise
+    // unbounded.
+    size_t max_possible_children = range.get_remaining_bytes() / 8;
+    if (max_number > max_possible_children) {
+      std::stringstream sstr;
+      sstr << "'" << get_type_string() << "' box declares " << max_number
+           << " child boxes, but the remaining data can hold at most "
+           << max_possible_children << ".";
+      return Error(heif_error_Invalid_input,
+                   heif_suberror_End_of_data,
+                   sstr.str());
+    }
+
+    m_children.reserve(max_number);
+  }

   while (!range.eof() && !range.error()) {
     std::shared_ptr<Box> box;
@@ -1117,14 +1162,6 @@ Error Box::read_children(BitstreamRange& range, uint32_t max_number, const heif_
     }

     if (max_number == READ_CHILDREN_ALL) {
-      uint32_t max_children;
-      if (get_short_type() == fourcc("iinf")) {
-        max_children = limits->max_items;
-      }
-      else {
-        max_children = limits->max_children_per_box;
-      }
-
       if (max_children && m_children.size() > max_children) {
         std::stringstream sstr;
         sstr << "Maximum number of child boxes (" << max_children << ") in '" << get_type_string() << "' box exceeded.";
@@ -1138,17 +1175,26 @@ Error Box::read_children(BitstreamRange& range, uint32_t max_number, const heif_

     m_children.push_back(std::move(box));

-
-    // count the new child and end reading new children when we reached the expected number
-
-    count++;
-
+    // Stop once we have read the expected number of children.
     if (max_number != READ_CHILDREN_ALL &&
-        count == max_number) {
+        m_children.size() == max_number) {
       break;
     }
   }

+  // If a specific number of children was expected, we must have read exactly
+  // that many. A short read means the box is truncated or malformed. Prefer
+  // this specific error over the lower-level range error below, which would
+  // otherwise mask it.
+  if (max_number != READ_CHILDREN_ALL && m_children.size() != max_number) {
+    std::stringstream sstr;
+    sstr << "'" << get_type_string() << "' box declares " << max_number
+         << " child boxes, but only " << m_children.size() << " could be read.";
+    return Error(heif_error_Invalid_input,
+                 heif_suberror_End_of_data,
+                 sstr.str());
+  }
+
   return range.get_error();
 }

@@ -2630,6 +2676,17 @@ Error Box_iinf::parse(BitstreamRange& range, const heif_security_limits* limits)
     return Error::Ok;
   }

+  // Sanity check.
+  if (limits->max_items && item_count > limits->max_items) {
+    std::stringstream sstr;
+    sstr << "iinf box contains " << item_count << " items, which exceeds the security limit of "
+         << limits->max_items << " items.";
+
+    return Error(heif_error_Memory_allocation_error,
+                 heif_suberror_Security_limit_exceeded,
+                 sstr.str());
+  }
+
   return read_children(range, item_count, limits);
 }