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)) {