Commit ea751a33 for libheif

commit ea751a33dbbfe8d0b92e929b66ebeba403fde7ae
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Sun Oct 25 19:16:20 2026 +0100

    Defer MIME metadata decompression until access [GHSA-c6m8-778m-cf4f]

diff --git a/examples/heif_info.cc b/examples/heif_info.cc
index 08004ae0..a6249aff 100644
--- a/examples/heif_info.cc
+++ b/examples/heif_info.cc
@@ -749,13 +749,23 @@ int main(int argc, char** argv)
       std::vector<heif_item_id> text_items(numTextItems);
       heif_image_handle_get_list_of_text_item_ids(handle, text_items.data(), numTextItems);
       for (heif_item_id text_item_id : text_items) {
-        struct heif_text_item* text_item;
+        struct heif_text_item* text_item = nullptr;
         err = heif_context_get_text_item(ctx.get(), text_item_id, &text_item);
+        if (err.code != heif_error_Ok || !text_item) {
+          printf("  text item: unavailable\n");
+          continue;
+        }
+
         const char* text_content = heif_text_item_get_content(text_item);
-        printf("  text item: %s\n", text_content);
-        heif_string_release(text_content);
+        if (text_content) {
+          printf("  text item: %s\n", text_content);
+          heif_string_release(text_content);
+        }
+        else {
+          printf("  text item: unavailable\n");
+        }

-        char* elng;
+        char* elng = nullptr;
         err = heif_item_get_property_extended_language(ctx.get(),
                                                        text_item_id,
                                                        &elng);
@@ -763,6 +773,7 @@ int main(int argc, char** argv)
           printf("    extended language: %s\n", elng);
         }
         heif_string_release(elng);
+        heif_text_item_release(text_item);
       }
     }
     else {
diff --git a/libheif/api/libheif/heif_metadata.cc b/libheif/api/libheif/heif_metadata.cc
index 8760cda4..72534a7e 100644
--- a/libheif/api/libheif/heif_metadata.cc
+++ b/libheif/api/libheif/heif_metadata.cc
@@ -126,6 +126,9 @@ size_t heif_image_handle_get_metadata_size(const heif_image_handle* handle,
 {
   for (auto& metadata : handle->image->get_metadata()) {
     if (metadata->item_id == metadata_id) {
+      if (metadata->ensure_loaded()) {
+        return 0;
+      }
       return metadata->m_data.size();
     }
   }
@@ -140,6 +143,9 @@ heif_error heif_image_handle_get_metadata(const heif_image_handle* handle,
 {
   for (auto& metadata : handle->image->get_metadata()) {
     if (metadata->item_id == metadata_id) {
+      if (Error err = metadata->ensure_loaded()) {
+        return err.error_struct(handle->image.get());
+      }
       if (!metadata->m_data.empty()) {
         if (out_data == nullptr) {
           return heif_error_null_pointer_argument;
diff --git a/libheif/api/libheif/heif_metadata.h b/libheif/api/libheif/heif_metadata.h
index 94ba76ce..2f901aa5 100644
--- a/libheif/api/libheif/heif_metadata.h
+++ b/libheif/api/libheif/heif_metadata.h
@@ -70,13 +70,15 @@ LIBHEIF_API
 const char* heif_image_handle_get_metadata_content_type(const heif_image_handle* handle,
                                                         heif_item_id metadata_id);

-// Get the size of the raw metadata, as stored in the HEIF file.
+// Get the size of the metadata content (decompressed for compressed MIME items).
+// This may read and decompress the item on first access. Returns 0 if the item
+// is missing or cannot be read.
 LIBHEIF_API
 size_t heif_image_handle_get_metadata_size(const heif_image_handle* handle,
                                            heif_item_id metadata_id);

 // 'out_data' must point to a memory area of the size reported by heif_image_handle_get_metadata_size().
-// The data is returned exactly as stored in the HEIF file.
+// Compressed MIME data is returned decompressed.
 // For Exif data, you probably have to skip the first four bytes of the data, since they
 // indicate the offset to the start of the TIFF header of the Exif data.
 LIBHEIF_API
diff --git a/libheif/api/libheif/heif_text.cc b/libheif/api/libheif/heif_text.cc
index 4b0d6c30..ddc75544 100644
--- a/libheif/api/libheif/heif_text.cc
+++ b/libheif/api/libheif/heif_text.cc
@@ -25,6 +25,7 @@
 #include "text.h"
 #include <algorithm>
 #include <cstring>
+#include <limits>
 #include <memory>
 #include <string>
 #include <utility>
@@ -113,10 +114,20 @@ const char* heif_text_item_get_content(heif_text_item* text_item)

   // return text as c-string

-  std::string txt = text_item->text_item->get_item_text();
+  auto txt_result = text_item->text_item->get_item_text();
+  if (!txt_result) {
+    return nullptr;
+  }
+  std::string_view txt = *txt_result;
+  if (txt.size() == std::numeric_limits<size_t>::max()) {
+    return nullptr;
+  }

-  char* text_c = new char[txt.length() + 1];
-  strcpy(text_c, txt.c_str());
+  char* text_c = new char[txt.size() + 1];
+  if (!txt.empty()) {
+    memcpy(text_c, txt.data(), txt.size());
+  }
+  text_c[txt.size()] = '\0';

   return text_c;
 }
@@ -159,4 +170,4 @@ heif_error heif_text_item_set_extended_language(heif_text_item* text_item, const
   }

   return heif_error_success;
-}
\ No newline at end of file
+}
diff --git a/libheif/api/libheif/heif_text.h b/libheif/api/libheif/heif_text.h
index 878e2d42..8c1e8d37 100644
--- a/libheif/api/libheif/heif_text.h
+++ b/libheif/api/libheif/heif_text.h
@@ -99,7 +99,7 @@ heif_item_id heif_text_item_get_id(heif_text_item* text_item);
  * This is the payload text, in the format given by the associated content_type.
  *
  * @param text_item the text item to query
- * @return the text item content (or null if the text_item is null). The returned string shall be released
+ * @return the text item content (or null if the item is null or cannot be read). The returned string shall be released
  * with heif_string_release().
  */
 LIBHEIF_API
diff --git a/libheif/context.cc b/libheif/context.cc
index bc5bbcd6..434195c3 100644
--- a/libheif/context.cc
+++ b/libheif/context.cc
@@ -1130,6 +1130,30 @@ Error HeifContext::interpret_heif_file_images()
       continue;
     }

+    // Items without a 'cdsc' association are not exposed as image metadata.
+    // In particular, do not decompress a MIME item merely because it is listed
+    // in iinf; an unreferenced item can be a decompression bomb.
+    if (!iref_box) {
+      continue;
+    }
+    std::vector<heif_item_id> references = iref_box->get_references(id, fourcc("cdsc"));
+    std::vector<std::shared_ptr<ImageItem>> target_images;
+    for (heif_item_id image_id : references) {
+      auto img_iter = m_all_images.find(image_id);
+      if (img_iter == m_all_images.end()) {
+        if (!m_heif_file->has_item_with_id(image_id)) {
+          return Error(heif_error_Invalid_input,
+                       heif_suberror_Nonexisting_item_referenced,
+                       "Metadata assigned to non-existing image");
+        }
+        continue;
+      }
+      target_images.push_back(img_iter->second);
+    }
+    if (target_images.empty()) {
+      continue;
+    }
+
     std::string item_uri_type = m_heif_file->get_item_uri_type(id);

     // we now assign all kinds of metadata to the image, not only 'Exif' and 'XMP'
@@ -1139,56 +1163,12 @@ Error HeifContext::interpret_heif_file_images()
     metadata->item_type = fourcc_to_string(item_type);
     metadata->content_type = content_type;
     metadata->item_uri_type = std::move(item_uri_type);
-
-    auto metadataResult = m_heif_file->get_uncompressed_item_data(id);
-    if (!metadataResult) {
-      if (metadataResult.error().error_code == heif_error_Unsupported_feature) {
-        // The item uses a content_encoding that we cannot decode (unknown coding, or the
-        // decompressor was not compiled in). That is a limitation of this one item, not of
-        // the file: skip it instead of refusing the whole file. Its raw data stays accessible
-        // through heif_item_get_item_data().
-        continue;
-      }
-
-      if (item_type == fourcc("Exif") || item_type == fourcc("mime")) {
-        // these item types should have data
-        return metadataResult.error();
-      }
-      else {
-        // anything else is probably something that we don't understand yet
-        continue;
-      }
-    }
-    else {
-      metadata->m_data = *metadataResult;
-
-      // Account the (possibly decompressed) metadata against the total-memory budget
-      // for the lifetime of the context. This bounds the cumulative memory of up to
-      // max_items metadata items, which would otherwise bypass the security limits
-      // (GHSA-24wx-9w62-c96w).
-      if (Error memErr = metadata->m_memory_handle.alloc(metadata->m_data.size(), &m_limits,
-                                                         "decompressed item metadata")) {
-        return memErr;
-      }
-    }
+    metadata->set_source(m_heif_file);

     // --- assign metadata to the image

-    if (iref_box) {
-      std::vector<heif_item_id> references = iref_box->get_references(id, fourcc("cdsc"));
-      for (heif_item_id exif_image_id : references) {
-        auto img_iter = m_all_images.find(exif_image_id);
-        if (img_iter == m_all_images.end()) {
-          if (!m_heif_file->has_item_with_id(exif_image_id)) {
-            return Error(heif_error_Invalid_input,
-                         heif_suberror_Nonexisting_item_referenced,
-                         "Metadata assigned to non-existing image");
-          }
-
-          continue;
-        }
-        img_iter->second->add_metadata(metadata);
-      }
+    for (const auto& image : target_images) {
+      image->add_metadata(metadata);
     }
   }

@@ -1299,40 +1279,30 @@ Error HeifContext::interpret_heif_file_images()
     if (item_type != fourcc("mime")) { // TODO: && content_type  starts with "text/" ?
       continue;
     }
-    std::shared_ptr<TextItem> text_item = std::make_shared<TextItem>();
-    text_item->set_item_id(id);
-
-    auto textDataResult = m_heif_file->get_uncompressed_item_data(id);
-    if (!textDataResult) {
-      if (textDataResult.error().error_code == heif_error_Unsupported_feature) {
-        // content_encoding that we cannot decode: this is not a text item we can use, but
-        // that is no reason to reject the file (see the metadata loop above)
-        continue;
+    if (!iref_box) {
+      continue;
+    }
+    std::vector<heif_item_id> target_image_ids;
+    for (const auto& ref : iref_box->get_references_from(id)) {
+      if (ref.header.get_short_type() == fourcc("text")) {
+        target_image_ids.insert(target_image_ids.end(), ref.to_item_ID.begin(), ref.to_item_ID.end());
       }
-
-      return textDataResult.error();
+    }
+    if (target_image_ids.empty()) {
+      continue;
     }

-    text_item->parse(*textDataResult);
-    if (iref_box) {
-      std::vector<Box_iref::Reference> references = iref_box->get_references_from(id);
-      for (const auto& ref : references) {
-        if (ref.header.get_short_type() == fourcc("text")) {
-          std::vector<uint32_t> refs = ref.to_item_ID;
-          for (uint32_t ref : refs) {
-            uint32_t image_id = ref;
-            auto img_iter = m_all_images.find(image_id);
-            if (img_iter == m_all_images.end()) {
-              return Error(heif_error_Invalid_input,
-                           heif_suberror_Nonexisting_item_referenced,
-                           "Text item assigned to non-existing image");
-            }
-            img_iter->second->add_text_item_id(id);
-            m_text_items.push_back(text_item);
-          }
-        }
+    std::shared_ptr<TextItem> text_item = std::make_shared<TextItem>(id, m_heif_file);
+    for (heif_item_id image_id : target_image_ids) {
+      auto img_iter = m_all_images.find(image_id);
+      if (img_iter == m_all_images.end()) {
+        return Error(heif_error_Invalid_input,
+                     heif_suberror_Nonexisting_item_referenced,
+                     "Text item assigned to non-existing image");
       }
+      img_iter->second->add_text_item_id(id);
     }
+    m_text_items.push_back(std::move(text_item));
   }

   return Error::Ok;
diff --git a/libheif/file.cc b/libheif/file.cc
index 4db68295..33789f9c 100644
--- a/libheif/file.cc
+++ b/libheif/file.cc
@@ -713,7 +713,40 @@ Error HeifFile::get_properties(heif_item_id imageID,
 }


-Result<std::vector<uint8_t>> HeifFile::get_uncompressed_item_data(heif_item_id ID) const
+Result<std::vector<uint8_t>> HeifFile::read_item_data_with_limit(heif_item_id ID,
+                                                                  size_t max_output_size) const
+{
+  // The ranged iloc reader expects the requested number of bytes to exist,
+  // so check the declared extent lengths before reading the complete item.
+  for (const auto& item : m_iloc_box->get_items()) {
+    if (item.item_ID == ID) {
+      uint64_t declared_size = 0;
+      for (const auto& extent : item.extents) {
+        if (extent.length > static_cast<uint64_t>(max_output_size) - declared_size) {
+          return Error(heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded,
+                       "Item data exceeds the requested maximum output size");
+        }
+        declared_size += extent.length;
+      }
+      break;
+    }
+  }
+
+  std::vector<uint8_t> data;
+  Error err = m_iloc_box->read_data(ID, m_input_stream, m_idat_box, &data, m_limits);
+  if (err) {
+    return err;
+  }
+  if (data.size() > max_output_size) {
+    return Error(heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded,
+                 "Item data exceeds the requested maximum output size");
+  }
+  return data;
+}
+
+
+Result<std::vector<uint8_t>> HeifFile::get_uncompressed_item_data(heif_item_id ID,
+                                                                   size_t max_output_size) const
 {
   assert(m_limits);

@@ -760,7 +793,7 @@ Result<std::vector<uint8_t>> HeifFile::get_uncompressed_item_data(heif_item_id I
           return error;
         }

-        return decompress_zlib(compressed_data, m_limits);
+        return decompress_zlib(compressed_data, m_limits, max_output_size);
 #else
         return Error(heif_error_Unsupported_feature,
                      heif_suberror_Unsupported_header_compression_method,
@@ -774,7 +807,7 @@ Result<std::vector<uint8_t>> HeifFile::get_uncompressed_item_data(heif_item_id I
         if (error) {
           return error;
         }
-        return decompress_deflate(compressed_data, m_limits);
+        return decompress_deflate(compressed_data, m_limits, max_output_size);
 #else
         return Error(heif_error_Unsupported_feature,
                      heif_suberror_Unsupported_header_compression_method,
@@ -788,7 +821,7 @@ Result<std::vector<uint8_t>> HeifFile::get_uncompressed_item_data(heif_item_id I
         if (error) {
           return error;
         }
-        return decompress_brotli(compressed_data, m_limits);
+        return decompress_brotli(compressed_data, m_limits, max_output_size);
 #else
         return Error(heif_error_Unsupported_feature,
                      heif_suberror_Unsupported_header_compression_method,
@@ -804,14 +837,7 @@ Result<std::vector<uint8_t>> HeifFile::get_uncompressed_item_data(heif_item_id I

   // --- read uncompressed

-  std::vector<uint8_t> data;
-  error = m_iloc_box->read_data(ID, m_input_stream, m_idat_box, &data, m_limits);
-  if (error) {
-    return error;
-  }
-  else {
-    return data;
-  }
+  return read_item_data_with_limit(ID, max_output_size);
 }


@@ -891,7 +917,8 @@ Error HeifFile::append_data_from_iloc(heif_item_id ID, std::vector<uint8_t>& out
 }


-Result<std::vector<uint8_t>> HeifFile::get_item_data(heif_item_id ID, heif_metadata_compression* out_compression) const
+Result<std::vector<uint8_t>> HeifFile::get_item_data(heif_item_id ID, heif_metadata_compression* out_compression,
+                                                     size_t max_output_size) const
 {
   Error error;

@@ -917,14 +944,7 @@ Result<std::vector<uint8_t>> HeifFile::get_item_data(heif_item_id ID, heif_metad
       *out_compression = heif_metadata_compression_off;
     }

-    std::vector<uint8_t> out_data;
-    Error err = m_iloc_box->read_data(ID, m_input_stream, m_idat_box, &out_data, m_limits);
-    if (err) {
-      return err;
-    }
-    else {
-      return out_data;
-    }
+    return read_item_data_with_limit(ID, max_output_size);
   }


@@ -941,14 +961,7 @@ Result<std::vector<uint8_t>> HeifFile::get_item_data(heif_item_id ID, heif_metad
       *out_compression = heif_metadata_compression_off;
     }

-    std::vector<uint8_t> out_data;
-    Error err = m_iloc_box->read_data(ID, m_input_stream, m_idat_box, &out_data, m_limits);
-    if (err) {
-      return err;
-    }
-    else {
-      return out_data;
-    }
+    return read_item_data_with_limit(ID, max_output_size);
   }
   else if (encoding == "compress_zlib") {
     compression = heif_metadata_compression_zlib;
@@ -963,6 +976,17 @@ Result<std::vector<uint8_t>> HeifFile::get_item_data(heif_item_id ID, heif_metad
     compression = heif_metadata_compression_unknown;
   }

+  // Return compressed bytes without decoding when the caller requests them.
+  // Their declared iloc size can be checked before allocating the buffer.
+  if (out_compression) {
+    auto raw_data = read_item_data_with_limit(ID, max_output_size);
+    if (!raw_data) {
+      return raw_data.error();
+    }
+    *out_compression = compression;
+    return raw_data;
+  }
+
   // read compressed data

   std::vector<uint8_t> compressed_data;
@@ -971,26 +995,18 @@ Result<std::vector<uint8_t>> HeifFile::get_item_data(heif_item_id ID, heif_metad
     return error;
   }

-  // return compressed data, if we do not want to have it uncompressed
-
-  const bool return_compressed = (out_compression != nullptr);
-  if (return_compressed) {
-    *out_compression = compression;
-    return compressed_data;
-  }
-
   // decompress the data

   switch (compression) {
 #if HAVE_ZLIB
     case heif_metadata_compression_zlib:
-      return decompress_zlib(compressed_data, m_limits);
+      return decompress_zlib(compressed_data, m_limits, max_output_size);
     case heif_metadata_compression_deflate:
-      return decompress_deflate(compressed_data, m_limits);
+      return decompress_deflate(compressed_data, m_limits, max_output_size);
 #endif
 #if HAVE_BROTLI
     case heif_metadata_compression_brotli:
-      return decompress_brotli(compressed_data, m_limits);
+      return decompress_brotli(compressed_data, m_limits, max_output_size);
 #endif
     default:
       return Error{heif_error_Unsupported_filetype, heif_suberror_Unsupported_header_compression_method};
diff --git a/libheif/file.h b/libheif/file.h
index 247778c2..a9e23daa 100644
--- a/libheif/file.h
+++ b/libheif/file.h
@@ -123,7 +123,8 @@ public:

   std::string get_item_uri_type(heif_item_id ID) const;

-  Result<std::vector<uint8_t>> get_uncompressed_item_data(heif_item_id ID) const;
+  Result<std::vector<uint8_t>> get_uncompressed_item_data(heif_item_id ID,
+                                                          size_t max_output_size = SIZE_MAX) const;

   Error append_data_from_file_range(std::vector<uint8_t>& out_data, uint64_t offset, uint32_t size) const;

@@ -135,7 +136,8 @@ public:

   // If `out_compression` is not NULL, the compression method is returned there and the compressed data is returned.
   // If `out_compression` is NULL, the data is returned decompressed.
-  Result<std::vector<uint8_t>> get_item_data(heif_item_id ID, heif_metadata_compression* out_compression) const;
+  Result<std::vector<uint8_t>> get_item_data(heif_item_id ID, heif_metadata_compression* out_compression,
+                                              size_t max_output_size = SIZE_MAX) const;

   std::shared_ptr<Box_ftyp> get_ftyp_box() { return m_ftyp_box; }

@@ -264,6 +266,8 @@ public:
   std::shared_ptr<Box_mvhd> get_mvhd_box() { return m_mvhd_box; }

 private:
+  Result<std::vector<uint8_t>> read_item_data_with_limit(heif_item_id ID,
+                                                          size_t max_output_size) const;
 #if ENABLE_PARALLEL_TILE_DECODING
   mutable std::mutex m_read_mutex;
 #endif
diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index 949ea782..9b529644 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -49,6 +49,36 @@
 #endif


+Error ImageMetadata::ensure_loaded(size_t max_output_size)
+{
+  std::lock_guard<std::mutex> lock(m_load_mutex);
+
+  if (m_source) {
+    auto data_result = m_source->get_uncompressed_item_data(item_id, max_output_size);
+    if (!data_result) {
+      return data_result.error();
+    }
+
+    MemoryHandle memory_handle;
+    if (Error err = memory_handle.alloc(data_result->capacity(), m_source->get_security_limits(),
+                                        "decompressed item metadata")) {
+      return err;
+    }
+
+    m_data = std::move(*data_result);
+    m_memory_handle = std::move(memory_handle);
+    m_source.reset();
+  }
+
+  if (m_data.size() > max_output_size) {
+    return Error(heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded,
+                 "Metadata item exceeds the requested maximum output size");
+  }
+
+  return Error::Ok;
+}
+
+
 ImageItem::ImageItem(HeifContext* context)
     : m_heif_context(context)
 {
diff --git a/libheif/image-items/image_item.h b/libheif/image-items/image_item.h
index f4cd9fe1..48963d6e 100644
--- a/libheif/image-items/image_item.h
+++ b/libheif/image-items/image_item.h
@@ -39,6 +39,7 @@


 class HeifContext;
+class HeifFile;

 class HeifPixelImage;

@@ -46,6 +47,10 @@ class HeifPixelImage;
 class ImageMetadata
 {
 public:
+  void set_source(std::shared_ptr<HeifFile> file) { m_source = std::move(file); }
+
+  Error ensure_loaded(size_t max_output_size = SIZE_MAX);
+
   heif_item_id item_id;
   std::string item_type;  // e.g. "Exif"
   std::string content_type;
@@ -59,6 +64,10 @@ public:
   // security limits (GHSA-24wx-9w62-c96w). MemoryHandle is move-only, which makes
   // ImageMetadata move-only; it is only ever held via shared_ptr, so that is fine.
   MemoryHandle m_memory_handle;
+
+private:
+  std::shared_ptr<HeifFile> m_source;
+  std::mutex m_load_mutex;
 };


diff --git a/libheif/text.cc b/libheif/text.cc
index 0cf7e187..e22bfa2d 100644
--- a/libheif/text.cc
+++ b/libheif/text.cc
@@ -19,21 +19,55 @@
  * along with libheif.  If not, see <http://www.gnu.org/licenses/>.
  */

-#include <string>
 #include "text.h"
 #include "error.h"
-#include <iostream>
+#include "file.h"
+#include <utility>


 Result<std::vector<uint8_t>> TextItem::encode() const
 {
-  std::vector<uint8_t> data;
-  data.assign(m_text.begin(), m_text.end());
-  return data;
+  return m_data;
 }

-Error TextItem::parse(const std::vector<uint8_t> &data)
+Error TextItem::parse(std::vector<uint8_t>&& data, const heif_security_limits* limits)
 {
-  m_text.assign(reinterpret_cast<const char*>(data.data()), data.size());
+  // The decompressor's temporary MemoryHandle is released when it returns.
+  // Keep the charge with the object that retains its output vector.
+  MemoryHandle new_memory_handle;
+  if (Error err = new_memory_handle.alloc(data.capacity(), limits, "decompressed text item")) {
+    return err;
+  }
+
+  m_data = std::move(data);
+  m_memory_handle = std::move(new_memory_handle);
+  m_source.reset();
   return Error::Ok;
-}
\ No newline at end of file
+}
+
+
+Result<std::string_view> TextItem::get_item_text(size_t max_output_size)
+{
+  std::lock_guard<std::mutex> lock(m_load_mutex);
+
+  if (m_source) {
+    auto data_result = m_source->get_uncompressed_item_data(m_item_id, max_output_size);
+    if (!data_result) {
+      return data_result.error();
+    }
+
+    if (Error err = parse(std::move(*data_result), m_source->get_security_limits())) {
+      return err;
+    }
+  }
+
+  if (m_data.size() > max_output_size) {
+    return Error(heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded,
+                 "Text item exceeds the requested maximum output size");
+  }
+
+  if (m_data.empty()) {
+    return std::string_view{};
+  }
+  return std::string_view{reinterpret_cast<const char*>(m_data.data()), m_data.size()};
+}
diff --git a/libheif/text.h b/libheif/text.h
index 0a85f4af..329a8269 100644
--- a/libheif/text.h
+++ b/libheif/text.h
@@ -23,19 +23,30 @@
 #define LIBHEIF_TEXT_H

 #include <cstring>
-#include <string>
+#include <memory>
+#include <mutex>
+#include <string_view>
 #include <vector>
 #include "error.h"
 #include "libheif/heif_text.h"
+#include "security_limits.h"
+
+class HeifFile;

 class TextItem
 {
 public:
   TextItem() = default;

-  TextItem(heif_item_id itemId, const char *text) : m_item_id(itemId), m_text(text) {}
+  TextItem(heif_item_id itemId, const char *text) : m_item_id(itemId)
+  {
+    m_data.assign(text, text + std::strlen(text));
+  }
+
+  TextItem(heif_item_id itemId, std::shared_ptr<HeifFile> file)
+      : m_item_id(itemId), m_source(std::move(file)) {}

-  Error parse(const std::vector<uint8_t> &data);
+  Error parse(std::vector<uint8_t>&& data, const heif_security_limits* limits);

   Result<std::vector<uint8_t>> encode() const;

@@ -47,11 +58,14 @@ public:
     m_item_id = id;
   }

-  std::string get_item_text() const { return m_text; }
+  Result<std::string_view> get_item_text(size_t max_output_size = SIZE_MAX);

 private:
   heif_item_id m_item_id = 0;
-  std::string m_text;
+  MemoryHandle m_memory_handle;
+  std::vector<uint8_t> m_data;
+  std::shared_ptr<HeifFile> m_source;
+  std::mutex m_load_mutex;
 };

 #endif // LIBHEIF_TEXT_H
diff --git a/tests/decompression_bomb.cc b/tests/decompression_bomb.cc
index 3b1e4669..5e458d12 100644
--- a/tests/decompression_bomb.cc
+++ b/tests/decompression_bomb.cc
@@ -24,16 +24,12 @@
   SOFTWARE.
 */

-// Regression test for GHSA-24wx-9w62-c96w: a compressed ('mime', content_encoding
-// "br") metadata item is a decompression bomb. Opening the file decompresses the
-// item in HeifContext::interpret_heif_file_images(), which used to grow an output
-// vector with no size accounting at all, bypassing the configured security limits
-// and allowing OOM from a few hundred bytes of input.
+// Regression tests for compressed MIME items: opening a file records associations
+// without expanding item contents, and explicit reads honor security limits.
 //
-// The test builds a tiny HEIF whose 'mime' item is a real brotli payload that
-// expands to tens of MB, then opens it once with a tight total-memory limit
-// (expecting a security-limit error) and once with the default limits (expecting
-// success, proving the file itself is valid and it is the limit that is enforced).
+// The test builds a tiny HEIF whose 'mime' item expands to tens of MB. Opening
+// succeeds even with a tight limit; reading the item then reports the limit
+// error. With default limits, the same item can be read.

 #include "catch_amalgamated.hpp"
 #include "libheif/heif.h"
@@ -52,7 +48,8 @@ namespace {
 //           codec plugin is required and the file opens without decoding.
 //   item 2 ('mime', content_encoding "br"): a brotli-compressed metadata item,
 //           'cdsc'-referencing item 1, carrying the supplied bomb payload.
-std::vector<uint8_t> build_heif_with_brotli_mime(const std::vector<uint8_t>& brotli_payload) {
+std::vector<uint8_t> build_heif_with_brotli_mime(const std::vector<uint8_t>& brotli_payload,
+                                                 bool attach_to_image = true) {
   std::vector<uint8_t> ftyp_payload;
   append_fourcc(ftyp_payload, "heic");
   put_u32_be(ftyp_payload, 0);
@@ -168,7 +165,9 @@ std::vector<uint8_t> build_heif_with_brotli_mime(const std::vector<uint8_t>& bro
   append(meta_payload, iinf);
   append(meta_payload, iprp);
   append(meta_payload, iloc);
-  append(meta_payload, iref);
+  if (attach_to_image) {
+    append(meta_payload, iref);
+  }
   append(meta_payload, idat);
   auto meta = make_box("meta", meta_payload, /*full=*/true);

@@ -178,9 +177,31 @@ std::vector<uint8_t> build_heif_with_brotli_mime(const std::vector<uint8_t>& bro
   return file;
 }

+
 } // namespace


+TEST_CASE("unreferenced compressed mime item is not decompressed during file open") {
+#if HAVE_BROTLI
+  const size_t decompressed_size = 16u * 1024 * 1024;
+  std::vector<uint8_t> zeros(decompressed_size, 0);
+  std::vector<uint8_t> bomb = compress_brotli(zeros.data(), zeros.size());
+  std::vector<uint8_t> file = build_heif_with_brotli_mime(bomb, false);
+
+  heif_context* ctx = heif_context_alloc();
+  REQUIRE(ctx != nullptr);
+  heif_security_limits* limits = heif_context_get_security_limits(ctx);
+  REQUIRE(limits != nullptr);
+  limits->max_total_memory = 8u * 1024 * 1024;
+
+  heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr);
+  REQUIRE(err.code == heif_error_Ok);
+  heif_context_free(ctx);
+#else
+  SUCCEED("brotli support not compiled in - skipping decompression bomb test");
+#endif
+}
+
 TEST_CASE("brotli mime metadata decompression bomb is bounded by security limits") {
 #if HAVE_BROTLI
   // A brotli payload that expands to ~40 MB but is only a few hundred bytes on disk.
@@ -192,8 +213,8 @@ TEST_CASE("brotli mime metadata decompression bomb is bounded by security limits

   std::vector<uint8_t> file = build_heif_with_brotli_mime(bomb);

-  // --- with a tight total-memory limit, opening must fail rather than allocate the
-  //     whole decompressed payload (previously it ignored the limit entirely).
+  // --- opening only records the metadata association. The limit is checked
+  //     when the application asks for its decompressed contents.
   {
     heif_context* ctx = heif_context_alloc();
     REQUIRE(ctx != nullptr);
@@ -203,8 +224,15 @@ TEST_CASE("brotli mime metadata decompression bomb is bounded by security limits
     limits->max_total_memory = 8u * 1024 * 1024; // 8 MB << 40 MB bomb

     heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr);
-    REQUIRE(err.code != heif_error_Ok);
+    REQUIRE(err.code == heif_error_Ok);
+
+    heif_image_handle* image = nullptr;
+    err = heif_context_get_primary_image_handle(ctx, &image);
+    REQUIRE(err.code == heif_error_Ok);
+    REQUIRE(heif_image_handle_get_metadata_size(image, 2) == 0);
+    err = heif_image_handle_get_metadata(image, 2, nullptr);
     REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded);
+    heif_image_handle_release(image);

     heif_context_free(ctx);
   }
@@ -218,6 +246,12 @@ TEST_CASE("brotli mime metadata decompression bomb is bounded by security limits
     heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr);
     REQUIRE(err.code == heif_error_Ok);

+    heif_image_handle* image = nullptr;
+    err = heif_context_get_primary_image_handle(ctx, &image);
+    REQUIRE(err.code == heif_error_Ok);
+    REQUIRE(heif_image_handle_get_metadata_size(image, 2) == decompressed_size);
+    heif_image_handle_release(image);
+
     heif_context_free(ctx);
   }
 #else
diff --git a/tests/text.cc b/tests/text.cc
index edebbff1..ffd2e448 100644
--- a/tests/text.cc
+++ b/tests/text.cc
@@ -29,9 +29,12 @@
 #include "libheif/heif.h"
 #include "test-config.h"
 #include "test_utils.h"
+#include "security_limits.h"
+#include "text.h"
 #include <cstddef>
 #include <cstdint>
 #include <cstring>
+#include <utility>
 #include <libheif/heif_items.h>
 #include <libheif/heif_library.h>

@@ -50,6 +53,30 @@ TEST_CASE("no text") {
   heif_context_free(context);
 }

+
+TEST_CASE("retained text data remains charged to the memory budget") {
+  heif_security_limits limits = global_security_limits;
+  limits.max_total_memory = 48;
+  TotalMemoryTracker tracker(&limits);
+
+  TextItem second;
+  std::vector<uint8_t> second_data(32, 'b');
+  {
+    TextItem first;
+    std::vector<uint8_t> first_data(32, 'a');
+    REQUIRE_FALSE(first.parse(std::move(first_data), &limits));
+    auto first_text = first.get_item_text();
+    REQUIRE(first_text);
+    REQUIRE(*first_text == std::string(32, 'a'));
+
+    Error err = second.parse(std::move(second_data), &limits);
+    REQUIRE(err.error_code == heif_error_Memory_allocation_error);
+    REQUIRE(err.sub_error_code == heif_suberror_Security_limit_exceeded);
+  }
+
+  REQUIRE_FALSE(second.parse(std::move(second_data), &limits));
+}
+
 TEST_CASE("create text item") {
   // skip test if we do not have the uncompressed codec
   if (!heif_have_decoder_for_format(heif_compression_uncompressed)) {