Commit f8ea4a46 for libheif
commit f8ea4a466922080d45c988f36ef29b3748776e1b
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Sun Oct 25 19:38:33 2026 +0100
Bound cumulative MIME metadata decompression work [GHSA-c6m8-778m-cf4f]
diff --git a/libheif/compression.h b/libheif/compression.h
index b5a0f993..d4883cec 100644
--- a/libheif/compression.h
+++ b/libheif/compression.h
@@ -23,12 +23,48 @@
#include <vector>
#include <cinttypes>
#include <cstddef>
+#include <mutex>
#include <error.h>
#include <libheif/heif_uncompressed.h>
struct heif_security_limits;
+// Counts bytes produced by MIME metadata decompression across all reads of one
+// file, including unsuccessful and repeated reads. Uncompressed image (unci)
+// decoding does not pass a budget to the decompressors.
+class MetadataDecompressionBudget
+{
+public:
+ void set_limit(const uint64_t* limit) { m_limit = limit; }
+
+ bool consume(size_t bytes)
+ {
+ const uint64_t limit = m_limit ? *m_limit : 0;
+ if (limit == 0 || bytes == 0) { return true; }
+ std::lock_guard<std::mutex> lock(m_mutex);
+ if (m_used >= limit || bytes > limit - m_used) {
+ // Saturate on an over-budget attempt so later reads fail immediately.
+ if (m_used < limit) { m_used = limit; }
+ return false;
+ }
+ m_used += bytes;
+ return true;
+ }
+
+ bool exhausted() const
+ {
+ const uint64_t limit = m_limit ? *m_limit : 0;
+ std::lock_guard<std::mutex> lock(m_mutex);
+ return limit != 0 && m_used >= limit;
+ }
+
+private:
+ const uint64_t* m_limit = nullptr;
+ mutable std::mutex m_mutex;
+ uint64_t m_used = 0;
+};
+
/**
* Convert heif_unci_compression enum to a fourcc code.
*
@@ -88,7 +124,8 @@ 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,
const heif_security_limits* limits,
- uint64_t max_output_size = UINT64_MAX);
+ uint64_t max_output_size = UINT64_MAX,
+ MetadataDecompressionBudget* work_budget = nullptr);
/**
* Decompress "deflate" compressed data.
@@ -107,7 +144,8 @@ Result<std::vector<uint8_t>> decompress_zlib(const std::vector<uint8_t>& compres
*/
Result<std::vector<uint8_t>> decompress_deflate(const std::vector<uint8_t>& compressed_input,
const heif_security_limits* limits,
- uint64_t max_output_size = UINT64_MAX);
+ uint64_t max_output_size = UINT64_MAX,
+ MetadataDecompressionBudget* work_budget = nullptr);
#endif
@@ -126,7 +164,8 @@ Result<std::vector<uint8_t>> decompress_deflate(const std::vector<uint8_t>& comp
*/
Result<std::vector<uint8_t>> decompress_brotli(const std::vector<uint8_t>& compressed_input,
const heif_security_limits* limits,
- uint64_t max_output_size = UINT64_MAX);
+ uint64_t max_output_size = UINT64_MAX,
+ MetadataDecompressionBudget* work_budget = nullptr);
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 24795a8b..dc71dbf4 100644
--- a/libheif/compression_brotli.cc
+++ b/libheif/compression_brotli.cc
@@ -36,8 +36,13 @@ const size_t BUF_SIZE = (1 << 18);
Result<std::vector<uint8_t>> decompress_brotli(const std::vector<uint8_t> &compressed_input,
const heif_security_limits* limits,
- uint64_t max_output_size)
+ uint64_t max_output_size,
+ MetadataDecompressionBudget* work_budget)
{
+ if (work_budget && work_budget->exhausted()) {
+ return Error(heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded,
+ "Total metadata decompression budget exceeded");
+ }
BrotliDecoderResult result = BROTLI_DECODER_RESULT_ERROR;
std::vector<uint8_t> buffer(BUF_SIZE, 0);
size_t available_in = compressed_input.size();
@@ -78,6 +83,13 @@ Result<std::vector<uint8_t>> decompress_brotli(const std::vector<uint8_t> &compr
{
result = BrotliDecoderDecompressStream(state.get(), &available_in, &next_in, &available_out, &next_output, 0);
+ // Count bytes produced on failed attempts and before output-size checks.
+ size_t produced = static_cast<size_t>(std::distance(buffer.data(), next_output));
+ if (work_budget && !work_budget->consume(produced)) {
+ return Error(heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded,
+ "Total metadata decompression budget exceeded");
+ }
+
if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT)
{
if (Error err = append_decoded_data()) {
diff --git a/libheif/compression_zlib.cc b/libheif/compression_zlib.cc
index 5eb91709..7fe8d209 100644
--- a/libheif/compression_zlib.cc
+++ b/libheif/compression_zlib.cc
@@ -83,8 +83,13 @@ 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,
const heif_security_limits* limits,
- uint64_t max_output_size)
+ uint64_t max_output_size,
+ MetadataDecompressionBudget* work_budget)
{
+ if (work_budget && work_budget->exhausted()) {
+ return Error(heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded,
+ "Total metadata decompression budget exceeded");
+ }
if (compressed_input.empty()) {
return Error(heif_error_Invalid_input, heif_suberror_Decompression_invalid_data,
"Empty zlib compressed data.");
@@ -130,6 +135,15 @@ Result<std::vector<uint8_t>> do_inflate(const std::vector<uint8_t>& compressed_i
err = inflate(&strm, Z_NO_FLUSH);
+ // Charge produced bytes even if the stream subsequently fails validation
+ // or the caller rejects the output size.
+ size_t n_new_bytes = dst.size() - strm.avail_out;
+ if (work_budget && !work_budget->consume(n_new_bytes)) {
+ inflateEnd(&strm);
+ return Error(heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded,
+ "Total metadata decompression budget exceeded");
+ }
+
if (err == Z_BUF_ERROR) {
if (strm.avail_in == 0) {
// All input consumed; decompression is complete even without Z_STREAM_END
@@ -162,8 +176,6 @@ Result<std::vector<uint8_t>> do_inflate(const std::vector<uint8_t>& compressed_i
// account for and append decoded data to output
- size_t n_new_bytes = dst.size() - strm.avail_out;
-
// Stop as soon as there is more data than the caller expects. This keeps a
// decompression bomb from inflating up to the security limits when the size
// of the decompressed data is known in advance (GHSA-fcmw-5764-7rq8).
@@ -200,15 +212,17 @@ 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,
const heif_security_limits* limits,
- uint64_t max_output_size)
+ uint64_t max_output_size,
+ MetadataDecompressionBudget* work_budget)
{
- return do_inflate(compressed_input, 15, limits, max_output_size);
+ return do_inflate(compressed_input, 15, limits, max_output_size, work_budget);
}
Result<std::vector<uint8_t>> decompress_deflate(const std::vector<uint8_t>& compressed_input,
const heif_security_limits* limits,
- uint64_t max_output_size)
+ uint64_t max_output_size,
+ MetadataDecompressionBudget* work_budget)
{
- return do_inflate(compressed_input, -15, limits, max_output_size);
+ return do_inflate(compressed_input, -15, limits, max_output_size, work_budget);
}
#endif
diff --git a/libheif/file.cc b/libheif/file.cc
index 33789f9c..4be75afd 100644
--- a/libheif/file.cc
+++ b/libheif/file.cc
@@ -793,7 +793,8 @@ Result<std::vector<uint8_t>> HeifFile::get_uncompressed_item_data(heif_item_id I
return error;
}
- return decompress_zlib(compressed_data, m_limits, max_output_size);
+ return decompress_zlib(compressed_data, m_limits, max_output_size,
+ &m_metadata_decompression_budget);
#else
return Error(heif_error_Unsupported_feature,
heif_suberror_Unsupported_header_compression_method,
@@ -807,7 +808,8 @@ 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, max_output_size);
+ return decompress_deflate(compressed_data, m_limits, max_output_size,
+ &m_metadata_decompression_budget);
#else
return Error(heif_error_Unsupported_feature,
heif_suberror_Unsupported_header_compression_method,
@@ -821,7 +823,8 @@ 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, max_output_size);
+ return decompress_brotli(compressed_data, m_limits, max_output_size,
+ &m_metadata_decompression_budget);
#else
return Error(heif_error_Unsupported_feature,
heif_suberror_Unsupported_header_compression_method,
@@ -1000,13 +1003,16 @@ 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, m_limits, max_output_size);
+ return decompress_zlib(compressed_data, m_limits, max_output_size,
+ &m_metadata_decompression_budget);
case heif_metadata_compression_deflate:
- return decompress_deflate(compressed_data, m_limits, max_output_size);
+ return decompress_deflate(compressed_data, m_limits, max_output_size,
+ &m_metadata_decompression_budget);
#endif
#if HAVE_BROTLI
case heif_metadata_compression_brotli:
- return decompress_brotli(compressed_data, m_limits, max_output_size);
+ return decompress_brotli(compressed_data, m_limits, max_output_size,
+ &m_metadata_decompression_budget);
#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 a9e23daa..081ae320 100644
--- a/libheif/file.h
+++ b/libheif/file.h
@@ -29,6 +29,7 @@
#include "image-items/vvc.h"
//#include "codecs/uncompressed/unc_boxes.h"
#include "file_layout.h"
+#include "compression.h"
#include <map>
#include <memory>
@@ -66,7 +67,10 @@ public:
// The limits will be shared from the HeifContext limits.
// You have to make sure that the pointer points to a valid object as long as the HeifFile is used.
- void set_security_limits(const heif_security_limits* limits) { m_limits = limits; }
+ void set_security_limits(const heif_security_limits* limits) {
+ m_limits = limits;
+ m_metadata_decompression_budget.set_limit(limits ? &limits->max_total_memory : nullptr);
+ }
const heif_security_limits* get_security_limits() const { return m_limits; }
@@ -308,6 +312,7 @@ private:
std::shared_ptr<Box_mvhd> m_mvhd_box;
const heif_security_limits* m_limits = nullptr;
+ mutable MetadataDecompressionBudget m_metadata_decompression_budget;
IDCreator m_id_creator;
Error parse_heif_file();
diff --git a/tests/decompression_bomb.cc b/tests/decompression_bomb.cc
index 5e458d12..ad112c74 100644
--- a/tests/decompression_bomb.cc
+++ b/tests/decompression_bomb.cc
@@ -33,6 +33,7 @@
#include "catch_amalgamated.hpp"
#include "libheif/heif.h"
+#include "libheif/heif_items.h"
#include "test_utils.h"
#include "compression.h"
@@ -202,6 +203,38 @@ TEST_CASE("unreferenced compressed mime item is not decompressed during file ope
#endif
}
+
+TEST_CASE("metadata decompression work is cumulative across item reads") {
+#if HAVE_BROTLI
+ const size_t output_size = 1024 * 1024;
+ std::vector<uint8_t> zeros(output_size, 0);
+ auto compressed = compress_brotli(zeros.data(), zeros.size());
+ auto file = build_heif_with_brotli_mime(compressed, false);
+
+ heif_context* ctx = heif_context_alloc();
+ REQUIRE(ctx != nullptr);
+ auto* limits = heif_context_get_security_limits(ctx);
+ limits->max_total_memory = output_size + output_size / 2;
+ REQUIRE(heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr).code == heif_error_Ok);
+
+ uint8_t* item_data = nullptr;
+ size_t item_size = 0;
+ heif_metadata_compression method;
+ REQUIRE(heif_item_get_item_data(ctx, 2, &method, &item_data, &item_size).code == heif_error_Ok);
+ REQUIRE(method == heif_metadata_compression_brotli);
+ heif_release_item_data(ctx, &item_data);
+
+ REQUIRE(heif_item_get_item_data(ctx, 2, nullptr, &item_data, &item_size).code == heif_error_Ok);
+ REQUIRE(item_size == output_size);
+ heif_release_item_data(ctx, &item_data);
+
+ auto err = heif_item_get_item_data(ctx, 2, nullptr, &item_data, &item_size);
+ REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded);
+ REQUIRE(item_data == nullptr);
+ heif_context_free(ctx);
+#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.