Commit 02084d74 for libheif

commit 02084d74519d53537f0cd9bd954f3e42f74764ee
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Sat May 16 12:19:38 2026 +0200

    register memory allocated on derived limits to the parent (#1801)

diff --git a/libheif/api/libheif/heif_security.h b/libheif/api/libheif/heif_security.h
index f63f86c3..0777baa0 100644
--- a/libheif/api/libheif/heif_security.h
+++ b/libheif/api/libheif/heif_security.h
@@ -73,6 +73,15 @@ typedef struct heif_security_limits
   // --- version 4

   uint32_t max_bad_pixels;
+
+  // --- version 5
+
+  // Internal: when libheif derives a limits struct from another one (e.g. to
+  // tighten the maximum image size for a specific decode), this points back to
+  // the registered context whose total-memory budget the allocation should be
+  // accounted against. nullptr means "this is a root context" (the registered
+  // one). User code should leave this as nullptr; the field is set internally.
+  const struct heif_security_limits* parent;
 } heif_security_limits;


diff --git a/libheif/context.cc b/libheif/context.cc
index 94c08391..408fd930 100644
--- a/libheif/context.cc
+++ b/libheif/context.cc
@@ -202,6 +202,10 @@ static void copy_security_limits(heif_security_limits* dst, const heif_security_
   if (src->version >= 4) {
     dst->max_bad_pixels = src->max_bad_pixels;
   }
+
+  // `parent` is an internal field; user-supplied limits are always treated as
+  // a root context. dst is HeifContext::m_limits, which is registered.
+  dst->parent = nullptr;
 }


diff --git a/libheif/security_limits.cc b/libheif/security_limits.cc
index c0074239..765ad2e1 100644
--- a/libheif/security_limits.cc
+++ b/libheif/security_limits.cc
@@ -25,7 +25,7 @@


 heif_security_limits global_security_limits{
-    .version = 4,
+    .version = 5,

     // --- version 1

@@ -55,12 +55,17 @@ heif_security_limits global_security_limits{

     .max_sequence_frames = 18'000'000,  // 100 hours at 50 fps
     .max_number_of_file_brands = 1000,
-    .max_bad_pixels = 1000
+    .max_bad_pixels = 1000,
+
+    // --- version 5
+
+    .parent = nullptr
 };


 heif_security_limits disabled_security_limits{
-    .version = 4
+    .version = 5,
+    .parent = nullptr
 };


@@ -86,6 +91,13 @@ heif_security_limits tighten_image_size_limit_for_ispe(const heif_security_limit
 {
   heif_security_limits result = *base;

+  // The returned struct is a stack-local derived copy. Point parent at the
+  // registered context so MemoryHandle::alloc() can still find the entry in
+  // sMemoryUsage for total-memory accounting. If base is itself derived, walk
+  // to the root so we keep the parent chain at one hop.
+  result.parent = (base->version >= 5 && base->parent) ? base->parent : base;
+  result.version = 5;
+
   if (ispe_width == 0 || ispe_height == 0) {
     return result;
   }
@@ -176,12 +188,6 @@ size_t TotalMemoryTracker::get_max_total_memory_used() const
 Error MemoryHandle::alloc(size_t memory_amount, const heif_security_limits* limits_context,
                           const char* reason_description)
 {
-  // we allow several allocations on the same handle, but they have to be for the same context
-  if (m_limits_context) {
-    assert(m_limits_context == limits_context);
-  }
-
-
   // --- check whether limits are exceeded

   if (!limits_context) {
@@ -208,15 +214,29 @@ Error MemoryHandle::alloc(size_t memory_amount, const heif_security_limits* limi
             sstr.str()};
   }

-  if (limits_context == &global_security_limits ||
-      limits_context == &disabled_security_limits) {
+  // Resolve to the registered (root) context for total-memory accounting.
+  // The passed-in limits may be a stack-local derived copy (e.g. tightened for
+  // ispe) whose `parent` points back to the registered context.
+  const heif_security_limits* root_limits = limits_context;
+  while (root_limits->version >= 5 && root_limits->parent) {
+    root_limits = root_limits->parent;
+  }
+
+  // we allow several allocations on the same handle, but they have to be for the same registered context
+  if (m_limits_context) {
+    assert(m_limits_context == root_limits);
+  }
+
+  if (root_limits == &global_security_limits ||
+      root_limits == &disabled_security_limits) {
     return Error::Ok;
   }

   std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
-  auto it = sMemoryUsage.find(limits_context);
+  auto it = sMemoryUsage.find(root_limits);
   if (it == sMemoryUsage.end()) {
-    assert(false);
+    // Unregistered limits context with no resolvable parent — total-memory
+    // tracking is not available, but the per-block check above still applies.
     return Error::Ok;
   }

@@ -245,7 +265,7 @@ Error MemoryHandle::alloc(size_t memory_amount, const heif_security_limits* limi

   // --- register memory usage

-  m_limits_context = limits_context;
+  m_limits_context = root_limits;
   m_memory_amount += memory_amount;

   it->second.total_memory_usage += memory_amount;