Commit 21893e8f for libheif
commit 21893e8f66c8b79363ca2809391b07d35e1a95ec
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon Aug 24 18:47:59 2026 +0200
bound brotli/zlib decompression output by security limits (GHSA-24wx-9w62-c96w)
diff --git a/libheif/codecs/uncompressed/unc_decoder.cc b/libheif/codecs/uncompressed/unc_decoder.cc
index bad7f158..9aeba61b 100644
--- a/libheif/codecs/uncompressed/unc_decoder.cc
+++ b/libheif/codecs/uncompressed/unc_decoder.cc
@@ -115,6 +115,10 @@ const Error unc_decoder::get_compressed_image_data_uncompressed(const DataExtent
std::shared_ptr<const Box_cmpC> cmpC_box = properties.cmpC;
std::shared_ptr<const Box_icef> icef_box = properties.icef;
+ // Security limits used to bound the (potentially highly amplified) decompressed
+ // output. May be nullptr for a raw data extent, in which case no limit applies.
+ const heif_security_limits* limits = dataExtent.m_file ? dataExtent.m_file->get_security_limits() : nullptr;
+
if (!cmpC_box) {
// assume no generic compression
auto readResult = dataExtent.read_data(range_start_offset, range_size);
@@ -148,7 +152,7 @@ const Error unc_decoder::get_compressed_image_data_uncompressed(const DataExtent
const std::vector<uint8_t>& compressed_bytes = *readingResult;
// decompress only the unit
- auto dataResult = do_decompress_data(cmpC_box, compressed_bytes);
+ auto dataResult = do_decompress_data(cmpC_box, compressed_bytes, limits);
if (!dataResult) {
return dataResult.error();
}
@@ -164,6 +168,14 @@ const Error unc_decoder::get_compressed_image_data_uncompressed(const DataExtent
const std::vector<uint8_t> compressed_bytes = std::move(**readResult);
+ // Bound the total decompressed output accumulated across all units. The units
+ // may overlap (nothing forces them to be disjoint), so N units can point at the
+ // same compressed slice and be decompressed N times into `data`. Without this
+ // accounting, that amplifies a tiny icef box into an unbounded allocation
+ // (GHSA-24wx-9w62-c96w). The handle is local, so it only bounds the peak while
+ // building `data`; the cropped result is accounted by the caller.
+ MemoryHandle accumulated_memory_handle;
+
for (Box_icef::CompressedUnitInfo unit_info : icef_box->get_units()) {
// Use subtraction form to avoid a uint64_t wrap in 'unit_offset + unit_size',
// which could otherwise pass this check and lead to an out-of-bounds read when
@@ -181,12 +193,18 @@ const Error unc_decoder::get_compressed_image_data_uncompressed(const DataExtent
auto unit_end = unit_start + unit_info.unit_size;
std::vector<uint8_t> compressed_unit_data = std::vector<uint8_t>(unit_start, unit_end);
- auto dataResult = do_decompress_data(cmpC_box, std::move(compressed_unit_data));
+ auto dataResult = do_decompress_data(cmpC_box, std::move(compressed_unit_data), limits);
if (!dataResult) {
return dataResult.error();
}
const std::vector<uint8_t> uncompressed_unit_data = std::move(*dataResult);
+
+ if (Error memErr = accumulated_memory_handle.alloc(uncompressed_unit_data.size(), limits,
+ "unci icef decompressed units")) {
+ return memErr;
+ }
+
data->insert(data->end(), uncompressed_unit_data.data(), uncompressed_unit_data.data() + uncompressed_unit_data.size());
}
@@ -213,7 +231,7 @@ const Error unc_decoder::get_compressed_image_data_uncompressed(const DataExtent
std::vector<uint8_t> compressed_bytes = std::move(**readResult);
// Decode as a single blob
- auto dataResult = do_decompress_data(cmpC_box, compressed_bytes);
+ auto dataResult = do_decompress_data(cmpC_box, compressed_bytes, limits);
if (!dataResult) {
return dataResult.error();
}
@@ -238,11 +256,12 @@ const Error unc_decoder::get_compressed_image_data_uncompressed(const DataExtent
Result<std::vector<uint8_t> > unc_decoder::do_decompress_data(std::shared_ptr<const Box_cmpC>& cmpC_box,
- std::vector<uint8_t> compressed_data) const
+ std::vector<uint8_t> compressed_data,
+ const heif_security_limits* limits) const
{
if (cmpC_box->get_compression_type() == fourcc("brot")) {
#if HAVE_BROTLI
- return decompress_brotli(compressed_data);
+ return decompress_brotli(compressed_data, limits);
#else
std::stringstream sstr;
sstr << "cannot decode unci item with brotli compression - not enabled" << std::endl;
@@ -253,7 +272,7 @@ Result<std::vector<uint8_t> > unc_decoder::do_decompress_data(std::shared_ptr<co
}
else if (cmpC_box->get_compression_type() == fourcc("zlib")) {
#if HAVE_ZLIB
- return decompress_zlib(compressed_data);
+ return decompress_zlib(compressed_data, limits);
#else
std::stringstream sstr;
sstr << "cannot decode unci item with zlib compression - not enabled" << std::endl;
@@ -264,7 +283,7 @@ Result<std::vector<uint8_t> > unc_decoder::do_decompress_data(std::shared_ptr<co
}
else if (cmpC_box->get_compression_type() == fourcc("defl")) {
#if HAVE_ZLIB
- return decompress_deflate(compressed_data);
+ return decompress_deflate(compressed_data, limits);
#else
std::stringstream sstr;
sstr << "cannot decode unci item with deflate compression - not enabled" << std::endl;
diff --git a/libheif/codecs/uncompressed/unc_decoder.h b/libheif/codecs/uncompressed/unc_decoder.h
index dc566ab0..49e4d994 100644
--- a/libheif/codecs/uncompressed/unc_decoder.h
+++ b/libheif/codecs/uncompressed/unc_decoder.h
@@ -75,7 +75,8 @@ protected:
const Box_iloc::Item* item) const;
Result<std::vector<uint8_t>> do_decompress_data(std::shared_ptr<const Box_cmpC>& cmpC_box,
- std::vector<uint8_t> compressed_data) const;
+ std::vector<uint8_t> compressed_data,
+ const heif_security_limits* limits) const;
const uint32_t m_width;
const uint32_t m_height;
diff --git a/libheif/compression.h b/libheif/compression.h
index 61c79766..38efca02 100644
--- a/libheif/compression.h
+++ b/libheif/compression.h
@@ -27,6 +27,8 @@
#include <error.h>
#include <libheif/heif_uncompressed.h>
+struct heif_security_limits;
+
/**
* Convert heif_unci_compression enum to a fourcc code.
*
@@ -81,7 +83,8 @@ std::vector<uint8_t> compress_deflate(const uint8_t* input, size_t size);
* @sa decompress_deflate
* @sa compress_zlib
*/
-Result<std::vector<uint8_t>> decompress_zlib(const std::vector<uint8_t>& compressed_input);
+Result<std::vector<uint8_t>> decompress_zlib(const std::vector<uint8_t>& compressed_input,
+ const heif_security_limits* limits);
/**
* Decompress "deflate" compressed data.
@@ -95,7 +98,8 @@ Result<std::vector<uint8_t>> decompress_zlib(const std::vector<uint8_t>& compres
* @sa decompress_zlib
* @sa compress_deflate
*/
-Result<std::vector<uint8_t>> decompress_deflate(const std::vector<uint8_t>& compressed_input);
+Result<std::vector<uint8_t>> decompress_deflate(const std::vector<uint8_t>& compressed_input,
+ const heif_security_limits* limits);
#endif
@@ -109,7 +113,8 @@ Result<std::vector<uint8_t>> decompress_deflate(const std::vector<uint8_t>& comp
* @param output pointer to the resulting vector of decompressed data
* @return success (Ok) or an error on failure (usually corrupt data)
*/
-Result<std::vector<uint8_t>> decompress_brotli(const std::vector<uint8_t>& compressed_input);
+Result<std::vector<uint8_t>> decompress_brotli(const std::vector<uint8_t>& compressed_input,
+ const heif_security_limits* limits);
std::vector<uint8_t> compress_brotli(const uint8_t* input, size_t size);
#endif
diff --git a/libheif/compression_brotli.cc b/libheif/compression_brotli.cc
index b6abc1fb..6bd4a085 100644
--- a/libheif/compression_brotli.cc
+++ b/libheif/compression_brotli.cc
@@ -31,9 +31,11 @@ const size_t BUF_SIZE = (1 << 18);
#include <vector>
#include "error.h"
+#include "security_limits.h"
-Result<std::vector<uint8_t>> decompress_brotli(const std::vector<uint8_t> &compressed_input)
+Result<std::vector<uint8_t>> decompress_brotli(const std::vector<uint8_t> &compressed_input,
+ const heif_security_limits* limits)
{
BrotliDecoderResult result = BROTLI_DECODER_RESULT_ERROR;
std::vector<uint8_t> buffer(BUF_SIZE, 0);
@@ -46,19 +48,32 @@ Result<std::vector<uint8_t>> decompress_brotli(const std::vector<uint8_t> &compr
std::vector<uint8_t> output;
+ // Track the growth of `output` against the security limits. Without this, a
+ // high-ratio "decompression bomb" would expand a few KB of input into GBs of
+ // output, bypassing max_memory_block_size / max_total_memory (GHSA-24wx-9w62-c96w).
+ MemoryHandle output_memory_handle;
+
while (true)
{
result = BrotliDecoderDecompressStream(state.get(), &available_in, &next_in, &available_out, &next_output, 0);
if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT)
{
- output.insert(output.end(), buffer.data(), buffer.data() + std::distance(buffer.data(), next_output));
+ size_t n_new_bytes = static_cast<size_t>(std::distance(buffer.data(), next_output));
+ if (Error memErr = output_memory_handle.alloc(n_new_bytes, limits, "brotli decompression output")) {
+ return memErr;
+ }
+ output.insert(output.end(), buffer.data(), buffer.data() + n_new_bytes);
available_out = buffer.size();
next_output = buffer.data();
}
else if (result == BROTLI_DECODER_RESULT_SUCCESS)
{
- output.insert(output.end(), buffer.data(), buffer.data() + std::distance(buffer.data(), next_output));
+ size_t n_new_bytes = static_cast<size_t>(std::distance(buffer.data(), next_output));
+ if (Error memErr = output_memory_handle.alloc(n_new_bytes, limits, "brotli decompression output")) {
+ return memErr;
+ }
+ output.insert(output.end(), buffer.data(), buffer.data() + n_new_bytes);
break;
}
else if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT)
diff --git a/libheif/compression_zlib.cc b/libheif/compression_zlib.cc
index e3543d89..2c145540 100644
--- a/libheif/compression_zlib.cc
+++ b/libheif/compression_zlib.cc
@@ -20,6 +20,7 @@
#include "compression.h"
+#include "security_limits.h"
#if HAVE_ZLIB
@@ -80,7 +81,8 @@ std::vector<uint8_t> compress(const uint8_t* input, size_t size, int windowSize)
}
-Result<std::vector<uint8_t>> do_inflate(const std::vector<uint8_t>& compressed_input, int windowSize)
+Result<std::vector<uint8_t>> do_inflate(const std::vector<uint8_t>& compressed_input, int windowSize,
+ const heif_security_limits* limits)
{
if (compressed_input.empty()) {
return Error(heif_error_Invalid_input, heif_suberror_Decompression_invalid_data,
@@ -89,6 +91,11 @@ Result<std::vector<uint8_t>> do_inflate(const std::vector<uint8_t>& compressed_i
std::vector<uint8_t> output;
+ // Track the growth of `output` against the security limits. Without this, a
+ // high-ratio "decompression bomb" would expand a few KB of input into GBs of
+ // output, bypassing max_memory_block_size / max_total_memory (GHSA-24wx-9w62-c96w).
+ MemoryHandle output_memory_handle;
+
// decompress data with zlib
std::vector<uint8_t> dst;
@@ -128,14 +135,18 @@ Result<std::vector<uint8_t>> do_inflate(const std::vector<uint8_t>& compressed_i
break;
}
- if (dst.size() >= 256 * 1024 * 1024) { // TODO: make this a security limit
+ // Grow the scratch buffer so inflate() can make progress. Bound its growth
+ // against the per-block security limit so a pathological stream cannot force
+ // an unbounded scratch allocation (the accumulated output is bounded
+ // separately, via output_memory_handle below).
+ size_t new_size = dst.size() * 2;
+ if (limits && limits->max_memory_block_size != 0 && new_size > limits->max_memory_block_size) {
inflateEnd(&strm);
- std::stringstream sstr;
- sstr << "Error performing zlib inflate: maximum output buffer size exceeded\n";
- return Error(heif_error_Memory_allocation_error, heif_suberror_Compression_initialisation_error, sstr.str());
+ return Error(heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded,
+ "zlib inflate scratch buffer exceeds the maximum block size");
}
- dst.resize(dst.size() * 2);
+ dst.resize(new_size);
strm.next_out = dst.data();
strm.avail_out = (uInt)dst.size();
continue;
@@ -148,8 +159,15 @@ Result<std::vector<uint8_t>> do_inflate(const std::vector<uint8_t>& compressed_i
return Error(heif_error_Invalid_input, heif_suberror_Decompression_invalid_data, sstr.str());
}
- // append decoded data to output
- output.insert(output.end(), dst.begin(), dst.end() - strm.avail_out);
+ // account for and append decoded data to output
+
+ size_t n_new_bytes = dst.size() - strm.avail_out;
+ if (Error memErr = output_memory_handle.alloc(n_new_bytes, limits, "zlib/deflate decompression output")) {
+ inflateEnd(&strm);
+ return memErr;
+ }
+
+ output.insert(output.end(), dst.begin(), dst.begin() + n_new_bytes);
} while (err != Z_STREAM_END);
@@ -169,13 +187,15 @@ std::vector<uint8_t> compress_deflate(const uint8_t* input, size_t size)
}
-Result<std::vector<uint8_t>> decompress_zlib(const std::vector<uint8_t>& compressed_input)
+Result<std::vector<uint8_t>> decompress_zlib(const std::vector<uint8_t>& compressed_input,
+ const heif_security_limits* limits)
{
- return do_inflate(compressed_input, 15);
+ return do_inflate(compressed_input, 15, limits);
}
-Result<std::vector<uint8_t>> decompress_deflate(const std::vector<uint8_t>& compressed_input)
+Result<std::vector<uint8_t>> decompress_deflate(const std::vector<uint8_t>& compressed_input,
+ const heif_security_limits* limits)
{
- return do_inflate(compressed_input, -15);
+ return do_inflate(compressed_input, -15, limits);
}
#endif
diff --git a/libheif/context.cc b/libheif/context.cc
index aafbcbd9..e4ce85a7 100644
--- a/libheif/context.cc
+++ b/libheif/context.cc
@@ -1111,6 +1111,15 @@ Error HeifContext::interpret_heif_file_images()
}
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;
+ }
}
// --- assign metadata to the image
diff --git a/libheif/file.cc b/libheif/file.cc
index eb90f61e..68eecfd6 100644
--- a/libheif/file.cc
+++ b/libheif/file.cc
@@ -773,7 +773,7 @@ Result<std::vector<uint8_t>> HeifFile::get_uncompressed_item_data(heif_item_id I
return error;
}
- return decompress_zlib(compressed_data);
+ return decompress_zlib(compressed_data, m_limits);
#else
return Error(heif_error_Unsupported_feature,
heif_suberror_Unsupported_header_compression_method,
@@ -787,7 +787,7 @@ Result<std::vector<uint8_t>> HeifFile::get_uncompressed_item_data(heif_item_id I
if (error) {
return error;
}
- return decompress_deflate(compressed_data);
+ return decompress_deflate(compressed_data, m_limits);
#else
return Error(heif_error_Unsupported_feature,
heif_suberror_Unsupported_header_compression_method,
@@ -801,7 +801,7 @@ Result<std::vector<uint8_t>> HeifFile::get_uncompressed_item_data(heif_item_id I
if (error) {
return error;
}
- return decompress_brotli(compressed_data);
+ return decompress_brotli(compressed_data, m_limits);
#else
return Error(heif_error_Unsupported_feature,
heif_suberror_Unsupported_header_compression_method,
@@ -997,13 +997,13 @@ Result<std::vector<uint8_t>> HeifFile::get_item_data(heif_item_id ID, heif_metad
switch (compression) {
#if HAVE_ZLIB
case heif_metadata_compression_zlib:
- return decompress_zlib(compressed_data);
+ return decompress_zlib(compressed_data, m_limits);
case heif_metadata_compression_deflate:
- return decompress_deflate(compressed_data);
+ return decompress_deflate(compressed_data, m_limits);
#endif
#if HAVE_BROTLI
case heif_metadata_compression_brotli:
- return decompress_brotli(compressed_data);
+ return decompress_brotli(compressed_data, m_limits);
#endif
default:
return Error{heif_error_Unsupported_filetype, heif_suberror_Unsupported_header_compression_method};
diff --git a/libheif/image-items/image_item.h b/libheif/image-items/image_item.h
index 5444ad89..be1c6431 100644
--- a/libheif/image-items/image_item.h
+++ b/libheif/image-items/image_item.h
@@ -49,6 +49,14 @@ public:
std::string content_type;
std::string item_uri_type;
std::vector<uint8_t> m_data;
+
+ // Accounts m_data against the context's total-memory budget for the lifetime of
+ // this metadata object. Metadata items may be (brotli/zlib) compressed and are
+ // held until the context is destroyed, so up to max_items of them accumulate.
+ // Without persistent accounting this cumulative decompressed memory bypasses the
+ // 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;
};
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 0c8d6c5f..80ed92ac 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -74,6 +74,12 @@ add_libheif_test(text)
add_libheif_test(cxx_wrapper)
add_libheif_test(component_descriptions)
+if (Brotli_FOUND)
+ add_libheif_test(decompression_bomb)
+else()
+ message(INFO " Disabling the decompression bomb test because Brotli was not found")
+endif ()
+
if (WITH_OPENJPH_ENCODER AND SUPPORTS_J2K_HT_ENCODING)
add_libheif_test(encode_htj2k)
else()
diff --git a/tests/decompression_bomb.cc b/tests/decompression_bomb.cc
new file mode 100644
index 00000000..4b40e7a6
--- /dev/null
+++ b/tests/decompression_bomb.cc
@@ -0,0 +1,270 @@
+/*
+ libheif unit tests
+
+ MIT License
+
+ Copyright (c) 2026 Dirk Farin <dirk.farin@gmail.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ 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.
+//
+// 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).
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+#include "compression.h"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace {
+
+void put_u32_be(std::vector<uint8_t>& out, uint32_t v) {
+ out.push_back(static_cast<uint8_t>(v >> 24));
+ out.push_back(static_cast<uint8_t>(v >> 16));
+ out.push_back(static_cast<uint8_t>(v >> 8));
+ out.push_back(static_cast<uint8_t>(v));
+}
+
+void put_u16_be(std::vector<uint8_t>& out, uint16_t v) {
+ out.push_back(static_cast<uint8_t>(v >> 8));
+ out.push_back(static_cast<uint8_t>(v));
+}
+
+void append_fourcc(std::vector<uint8_t>& out, const char fourcc[4]) {
+ out.insert(out.end(), fourcc, fourcc + 4);
+}
+
+// Append a null-terminated string (as read by Box_infe via read_string()).
+void append_cstr(std::vector<uint8_t>& out, const char* s) {
+ while (*s) { out.push_back(static_cast<uint8_t>(*s)); ++s; }
+ out.push_back(0);
+}
+
+void append(std::vector<uint8_t>& out, const std::vector<uint8_t>& v) {
+ out.insert(out.end(), v.begin(), v.end());
+}
+
+std::vector<uint8_t> make_box(const char fourcc[4],
+ const std::vector<uint8_t>& payload,
+ bool is_full_box = false,
+ uint8_t version = 0,
+ uint32_t flags = 0) {
+ std::vector<uint8_t> body;
+ if (is_full_box) {
+ body.push_back(version);
+ body.push_back(static_cast<uint8_t>(flags >> 16));
+ body.push_back(static_cast<uint8_t>(flags >> 8));
+ body.push_back(static_cast<uint8_t>(flags));
+ }
+ body.insert(body.end(), payload.begin(), payload.end());
+
+ std::vector<uint8_t> box;
+ put_u32_be(box, static_cast<uint32_t>(8 + body.size()));
+ append_fourcc(box, fourcc);
+ box.insert(box.end(), body.begin(), body.end());
+ return box;
+}
+
+// Build a minimal HEIF file with two items:
+// item 1 ('mski', 8x8, 8bpp): the primary image. The mask codec needs no
+// bitstream (its "compressed" data is just the raw pixel bytes), so no
+// 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> ftyp_payload;
+ append_fourcc(ftyp_payload, "heic");
+ put_u32_be(ftyp_payload, 0);
+ append_fourcc(ftyp_payload, "mif1");
+ append_fourcc(ftyp_payload, "heic");
+ auto ftyp = make_box("ftyp", ftyp_payload);
+
+ std::vector<uint8_t> hdlr_payload;
+ put_u32_be(hdlr_payload, 0);
+ append_fourcc(hdlr_payload, "pict");
+ put_u32_be(hdlr_payload, 0);
+ put_u32_be(hdlr_payload, 0);
+ put_u32_be(hdlr_payload, 0);
+ hdlr_payload.push_back(0);
+ auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true);
+
+ // pitm: primary item = item 1 (the 'mski' image).
+ std::vector<uint8_t> pitm_payload;
+ put_u16_be(pitm_payload, 1);
+ auto pitm = make_box("pitm", pitm_payload, /*full=*/true);
+
+ // iinf: item 1 = 'mski', item 2 = 'mime'.
+ std::vector<uint8_t> infe1_payload;
+ put_u16_be(infe1_payload, 1);
+ put_u16_be(infe1_payload, 0);
+ append_fourcc(infe1_payload, "mski");
+ infe1_payload.push_back(0); // item_name (empty)
+ auto infe1 = make_box("infe", infe1_payload, /*full=*/true, /*version=*/2);
+
+ std::vector<uint8_t> infe2_payload;
+ put_u16_be(infe2_payload, 2);
+ put_u16_be(infe2_payload, 0);
+ append_fourcc(infe2_payload, "mime");
+ append_cstr(infe2_payload, ""); // item_name
+ append_cstr(infe2_payload, "application/octet-stream"); // content_type
+ append_cstr(infe2_payload, "br"); // content_encoding
+ auto infe2 = make_box("infe", infe2_payload, /*full=*/true, /*version=*/2);
+
+ std::vector<uint8_t> iinf_payload;
+ put_u16_be(iinf_payload, 2);
+ append(iinf_payload, infe1);
+ append(iinf_payload, infe2);
+ auto iinf = make_box("iinf", iinf_payload, /*full=*/true);
+
+ // iprp / ipco: prop 1 = ispe(8,8), prop 2 = mskC(8bpp), both for item 1.
+ std::vector<uint8_t> ispe_payload;
+ put_u32_be(ispe_payload, 8);
+ put_u32_be(ispe_payload, 8);
+ auto ispe = make_box("ispe", ispe_payload, /*full=*/true);
+
+ std::vector<uint8_t> mskC_payload;
+ mskC_payload.push_back(8); // bits_per_pixel
+ auto mskC = make_box("mskC", mskC_payload, /*full=*/true);
+
+ std::vector<uint8_t> ipco_payload;
+ append(ipco_payload, ispe);
+ append(ipco_payload, mskC);
+ auto ipco = make_box("ipco", ipco_payload);
+
+ std::vector<uint8_t> ipma_payload;
+ put_u32_be(ipma_payload, 1); // entry_count
+ put_u16_be(ipma_payload, 1); // item_ID 1
+ ipma_payload.push_back(2); // association_count
+ ipma_payload.push_back(0x80 | 1); // essential, ispe
+ ipma_payload.push_back(0x80 | 2); // essential, mskC
+ auto ipma = make_box("ipma", ipma_payload, /*full=*/true);
+
+ std::vector<uint8_t> iprp_payload;
+ append(iprp_payload, ipco);
+ append(iprp_payload, ipma);
+ auto iprp = make_box("iprp", iprp_payload);
+
+ // idat: 64 bytes of raw 8x8 8bpp mask data, followed by the brotli bomb payload.
+ std::vector<uint8_t> idat_payload(64, 0x7F);
+ const uint32_t bomb_offset = static_cast<uint32_t>(idat_payload.size());
+ const uint32_t bomb_length = static_cast<uint32_t>(brotli_payload.size());
+ idat_payload.insert(idat_payload.end(), brotli_payload.begin(), brotli_payload.end());
+ auto idat = make_box("idat", idat_payload);
+
+ // iloc (version 1): both items store their data in idat (construction_method=1).
+ std::vector<uint8_t> iloc_payload;
+ put_u16_be(iloc_payload, (4 << 12) | (4 << 8) | (0 << 4) | 0); // offset_size=4, length_size=4
+ put_u16_be(iloc_payload, 2); // item_count
+ // item 1: mask pixels
+ put_u16_be(iloc_payload, 1); // item_ID
+ put_u16_be(iloc_payload, 0x0001); // reserved(12) + construction_method=1 (idat)
+ put_u16_be(iloc_payload, 0); // data_reference_index
+ put_u16_be(iloc_payload, 1); // extent_count
+ put_u32_be(iloc_payload, 0); // extent_offset (within idat)
+ put_u32_be(iloc_payload, 64); // extent_length
+ // item 2: brotli bomb
+ put_u16_be(iloc_payload, 2); // item_ID
+ put_u16_be(iloc_payload, 0x0001); // reserved(12) + construction_method=1 (idat)
+ put_u16_be(iloc_payload, 0); // data_reference_index
+ put_u16_be(iloc_payload, 1); // extent_count
+ put_u32_be(iloc_payload, bomb_offset);// extent_offset (within idat)
+ put_u32_be(iloc_payload, bomb_length);// extent_length
+ auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1);
+
+ // iref: item 2 ('mime') 'cdsc'-references item 1 (metadata describes the image).
+ std::vector<uint8_t> cdsc_payload;
+ put_u16_be(cdsc_payload, 2); // from_item_ID
+ put_u16_be(cdsc_payload, 1); // reference_count
+ put_u16_be(cdsc_payload, 1); // to_item_ID
+ auto cdsc = make_box("cdsc", cdsc_payload);
+ auto iref = make_box("iref", cdsc, /*full=*/true);
+
+ std::vector<uint8_t> meta_payload;
+ append(meta_payload, hdlr);
+ append(meta_payload, pitm);
+ append(meta_payload, iinf);
+ append(meta_payload, iprp);
+ append(meta_payload, iloc);
+ append(meta_payload, iref);
+ append(meta_payload, idat);
+ auto meta = make_box("meta", meta_payload, /*full=*/true);
+
+ std::vector<uint8_t> file;
+ append(file, ftyp);
+ append(file, meta);
+ return file;
+}
+
+} // namespace
+
+
+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.
+ const size_t decompressed_size = 40u * 1024 * 1024;
+ std::vector<uint8_t> zeros(decompressed_size, 0);
+ std::vector<uint8_t> bomb = compress_brotli(zeros.data(), zeros.size());
+ REQUIRE(!bomb.empty());
+ REQUIRE(bomb.size() < decompressed_size); // it really is highly compressible
+
+ 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).
+ {
+ 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; // 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.subcode == heif_suberror_Security_limit_exceeded);
+
+ heif_context_free(ctx);
+ }
+
+ // --- with the default (generous) limits, the very same file opens successfully,
+ // proving it is structurally valid and that the limit is what is enforced.
+ {
+ heif_context* ctx = heif_context_alloc();
+ REQUIRE(ctx != nullptr);
+
+ 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
+}