Commit d0c3c693 for libheif
commit d0c3c69326f320ba8a86607b8f702c5ef4e93486
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon Oct 5 13:58:21 2026 +0200
Limit the number of entries printed per box in dump()
dump() prints one or more lines per entry for the boxes whose content
scales with the file size (the sample tables stts, ctts, stsc, stco,
stsz, stss, sdtp, elst, saiz, saio, sbgp, sgpd). An 'sdtp' box holds one
byte per sample and prints four lines each, so a roughly 1 MB box
produced a ~200 MB dump, and nesting it in 'j2kH' containers multiplied
the work. The box_fuzzer timed out on such a file (reported by OSS-Fuzz).
These boxes now print at most MAX_DUMP_ENTRIES (100) entries followed by
a note about how many were omitted, unless the caller passes
full_log=true. heif-info's box dump and the public
heif_context_debug_dump_boxes_to_file() use the bounded form, so an
untrusted file can no longer blow up the dump.
The test builds a 'j2kH' holding an 'sdtp' with many entries and checks
that the default dump is small (and shows the truncation note) while the
full dump still contains every entry. The fuzzer input is added to the
seed corpus.
diff --git a/fuzzing/data/corpus/j2kH-sdtp-dump-timeout.heic b/fuzzing/data/corpus/j2kH-sdtp-dump-timeout.heic
new file mode 100644
index 00000000..af3c97f3
Binary files /dev/null and b/fuzzing/data/corpus/j2kH-sdtp-dump-timeout.heic differ
diff --git a/libheif/logging.h b/libheif/logging.h
index 0a765d11..a4169d2d 100644
--- a/libheif/logging.h
+++ b/libheif/logging.h
@@ -32,6 +32,14 @@
#include <ostream>
+// dump() is a debugging aid. Boxes whose content scales with the file size (the
+// sample tables, reference lists, ...) can otherwise produce gigabytes of text
+// from a small input, which nested containers then copy at every level. Unless
+// the caller passes full_log=true, such boxes print at most this many entries
+// and then a short note about how many were omitted.
+static const size_t MAX_DUMP_ENTRIES = 100;
+
+
class Indent
{
public:
@@ -72,6 +80,22 @@ inline void reset_stream_format(std::ostream& ostr)
}
+// Helper for dump() loops over a container whose length scales with the file
+// size. Call it at the top of the loop body with the current index and the
+// total count. When full_log is false and MAX_DUMP_ENTRIES items have already
+// been written, it writes a short note about the remaining items and returns
+// true, telling the loop to stop.
+inline bool dump_reached_entry_limit(std::ostream& ostr, const Indent& indent,
+ bool full_log, size_t index, size_t total)
+{
+ if (full_log || index < MAX_DUMP_ENTRIES) {
+ return false;
+ }
+
+ ostr << indent << "... (" << (total - index) << " more)\n";
+ return true;
+}
+
std::string write_raw_data_as_hex(const uint8_t* data, size_t len,
const std::string& firstLineIndent,
const std::string& remainingLinesIndent);
diff --git a/libheif/sequences/seq_boxes.cc b/libheif/sequences/seq_boxes.cc
index edbb2995..add0f703 100644
--- a/libheif/sequences/seq_boxes.cc
+++ b/libheif/sequences/seq_boxes.cc
@@ -622,6 +622,7 @@ void Box_stts::dump(std::ostream& sstr, Indent& indent, bool full_log) const
{
FullBox::dump(sstr, indent, full_log);
for (size_t i = 0; i < m_entries.size(); i++) {
+ if (dump_reached_entry_limit(sstr, indent, full_log, i, m_entries.size())) break;
sstr << indent << "[" << i << "] : cnt=" << m_entries[i].sample_count << ", delta=" << m_entries[i].sample_delta << "\n";
}
@@ -815,6 +816,7 @@ void Box_ctts::dump(std::ostream& sstr, Indent& indent, bool full_log) const
{
FullBox::dump(sstr, indent, full_log);
for (size_t i = 0; i < m_entries.size(); i++) {
+ if (dump_reached_entry_limit(sstr, indent, full_log, i, m_entries.size())) break;
sstr << indent << "[" << i << "] : cnt=" << m_entries[i].sample_count << ", offset=" << m_entries[i].sample_offset << "\n";
}
@@ -984,6 +986,7 @@ void Box_stsc::dump(std::ostream& sstr, Indent& indent, bool full_log) const
{
FullBox::dump(sstr, indent, full_log);
for (size_t i = 0; i < m_entries.size(); i++) {
+ if (dump_reached_entry_limit(sstr, indent, full_log, i, m_entries.size())) break;
sstr << indent << "[" << i << "]\n"
<< indent << " first chunk: " << m_entries[i].first_chunk << "\n"
<< indent << " samples per chunk: " << m_entries[i].samples_per_chunk << "\n"
@@ -1085,6 +1088,7 @@ void Box_stco::dump(std::ostream& sstr, Indent& indent, bool full_log) const
{
FullBox::dump(sstr, indent, full_log);
for (size_t i = 0; i < m_offsets.size(); i++) {
+ if (dump_reached_entry_limit(sstr, indent, full_log, i, m_offsets.size())) break;
sstr << indent << "[" << i << "] : 0x" << std::hex << m_offsets[i] << std::dec << "\n";
}
@@ -1188,6 +1192,7 @@ void Box_stsz::dump(std::ostream& sstr, Indent& indent, bool full_log) const
sstr << indent << "sample count: " << m_sample_count << "\n";
if (m_fixed_sample_size == 0) {
for (size_t i = 0; i < m_sample_sizes.size(); i++) {
+ if (dump_reached_entry_limit(sstr, indent, full_log, i, m_sample_sizes.size())) break;
sstr << indent << "[" << i << "] : " << m_sample_sizes[i] << "\n";
}
}
@@ -1280,6 +1285,7 @@ void Box_stss::dump(std::ostream& sstr, Indent& indent, bool full_log) const
{
FullBox::dump(sstr, indent, full_log);
for (size_t i = 0; i < m_sync_samples.size(); i++) {
+ if (dump_reached_entry_limit(sstr, indent, full_log, i, m_sync_samples.size())) break;
sstr << indent << "[" << i << "] : " << m_sync_samples[i] << "\n";
}
@@ -1606,9 +1612,14 @@ void Box_sbgp::dump(std::ostream& sstr, Indent& indent, bool full_log) const
uint32_t total_samples = 0;
for (size_t i = 0; i < m_entries.size(); i++) {
- sstr << indent << "[" << std::setw(2) << (i + 1) << "] : " << std::setw(3) << m_entries[i].sample_count << "x " << m_entries[i].group_description_index << "\n";
+ if (full_log || i < MAX_DUMP_ENTRIES) {
+ sstr << indent << "[" << std::setw(2) << (i + 1) << "] : " << std::setw(3) << m_entries[i].sample_count << "x " << m_entries[i].group_description_index << "\n";
+ }
total_samples += m_entries[i].sample_count;
}
+ if (!full_log && m_entries.size() > MAX_DUMP_ENTRIES) {
+ sstr << indent << "... (" << (m_entries.size() - MAX_DUMP_ENTRIES) << " more)\n";
+ }
sstr << indent << "total samples: " << total_samples << "\n";
return;
@@ -1750,6 +1761,7 @@ void Box_sgpd::dump(std::ostream& sstr, Indent& indent, bool full_log) const
}
for (size_t i=0; i<m_entries.size(); i++) {
+ if (dump_reached_entry_limit(sstr, indent, full_log, i, m_entries.size())) break;
sstr << indent << "[" << (i+1) << "] : ";
if (m_entries[i].sample_group_entry) {
sstr << m_entries[i].sample_group_entry->dump() << "\n";
@@ -1969,6 +1981,7 @@ void Box_saiz::dump(std::ostream& sstr, Indent& indent, bool full_log) const
if (m_default_sample_info_size == 0) {
for (size_t i = 0; i < m_sample_sizes.size(); i++) {
+ if (dump_reached_entry_limit(sstr, indent, full_log, i, m_sample_sizes.size())) break;
sstr << indent << "[" << i << "] : " << ((int) m_sample_sizes[i]) << "\n";
}
}
@@ -2104,6 +2117,7 @@ void Box_saio::dump(std::ostream& sstr, Indent& indent, bool full_log) const
}
for (size_t i = 0; i < m_chunk_offset.size(); i++) {
+ if (dump_reached_entry_limit(sstr, indent, full_log, i, m_chunk_offset.size())) break;
sstr << indent << "[" << i << "] : 0x" << std::hex << m_chunk_offset[i] << "\n";
}
@@ -2219,6 +2233,8 @@ void Box_sdtp::dump(std::ostream& sstr, Indent& indent, bool full_log) const
for (uint32_t i = 0; i < static_cast<uint32_t>(m_sample_information.size()); i++) {
+ if (dump_reached_entry_limit(sstr, indent, full_log, i, m_sample_information.size())) break;
+
const char* spaces = " ";
int nSpaces = 6;
int k = i;
@@ -2493,7 +2509,9 @@ void Box_elst::dump(std::ostream& sstr, Indent& indent, bool full_log) const
sstr << indent << "repeat list: " << ((get_flags() & Flags::Repeat_EditList) ? "yes" : "no") << "\n";
- for (const auto& entry : m_entries) {
+ for (size_t i = 0; i < m_entries.size(); i++) {
+ if (dump_reached_entry_limit(sstr, indent, full_log, i, m_entries.size())) break;
+ const auto& entry = m_entries[i];
sstr << indent << "segment duration: " << entry.segment_duration << "\n";
sstr << indent << "media time: " << entry.media_time << "\n";
sstr << indent << "media rate integer: " << entry.media_rate_integer << "\n";
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index d64a1e31..38c877ec 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -54,6 +54,7 @@ else()
add_libheif_test(fill_channel)
add_libheif_test(duplicate_alpha_channel)
add_libheif_test(idat)
+add_libheif_test(box_dump_limit)
add_libheif_test(scale_plane_checks)
add_libheif_test(crop_plane_checks)
add_libheif_test(extract_area_plane_checks)
diff --git a/tests/box_dump_limit.cc b/tests/box_dump_limit.cc
new file mode 100644
index 00000000..679061c3
--- /dev/null
+++ b/tests/box_dump_limit.cc
@@ -0,0 +1,110 @@
+/*
+ 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.
+*/
+
+// Box::dump() is a debugging aid. Boxes whose content scales with the file size
+// (here the 'sdtp' sample-dependency table, one entry per byte) print four lines
+// per entry, and a 'j2kH' container copies that text at every nesting level. A
+// ~1 MB file therefore produced a ~200 MB dump string, which timed out the
+// box_fuzzer (OSS-Fuzz, GHSA-h4h8-qgvc-m7r2).
+//
+// dump() now (a) writes into a single stream instead of returning and
+// re-concatenating a string at every level, and (b) prints at most
+// MAX_DUMP_ENTRIES entries per box unless full_log is set. This test builds a
+// 'j2kH' holding an 'sdtp' with many entries and checks that the default dump is
+// small while the full dump still contains every entry.
+
+#include "catch_amalgamated.hpp"
+#include "box.h"
+#include "logging.h"
+
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+namespace {
+
+void append_u32(std::vector<uint8_t>& v, uint32_t x)
+{
+ v.push_back(uint8_t(x >> 24));
+ v.push_back(uint8_t(x >> 16));
+ v.push_back(uint8_t(x >> 8));
+ v.push_back(uint8_t(x));
+}
+
+// A 'j2kH' box containing an 'sdtp' box with `num_samples` one-byte entries.
+std::vector<uint8_t> build_j2kH_with_sdtp(uint32_t num_samples)
+{
+ std::vector<uint8_t> sdtp;
+ append_u32(sdtp, 8 + 4 + num_samples); // box size
+ for (char c : std::string("sdtp")) sdtp.push_back(uint8_t(c));
+ append_u32(sdtp, 0); // FullBox version/flags
+ sdtp.insert(sdtp.end(), num_samples, 0); // one byte per sample
+
+ std::vector<uint8_t> j2kH;
+ append_u32(j2kH, 8 + uint32_t(sdtp.size())); // box size
+ for (char c : std::string("j2kH")) j2kH.push_back(uint8_t(c));
+ j2kH.insert(j2kH.end(), sdtp.begin(), sdtp.end());
+
+ return j2kH;
+}
+
+std::shared_ptr<Box> read_first_box(const std::vector<uint8_t>& data)
+{
+ auto reader = std::make_shared<StreamReader_memory>(data.data(), data.size(), false);
+ BitstreamRange range(reader, data.size());
+
+ std::shared_ptr<Box> box;
+ Error err = Box::read(range, &box, heif_get_global_security_limits());
+ REQUIRE(err == Error::Ok);
+ REQUIRE(box);
+ return box;
+}
+
+} // namespace
+
+
+TEST_CASE("box dump: verbose boxes are bounded unless full_log is set")
+{
+ const uint32_t num_samples = 50000; // would be 4 lines each in the dump
+ std::shared_ptr<Box> box = read_first_box(build_j2kH_with_sdtp(num_samples));
+
+ Indent indent_default;
+ std::string dflt = box->dump_to_string(indent_default);
+
+ Indent indent_full;
+ std::string full = box->dump_to_string(indent_full, true);
+
+ // The default dump stops after MAX_DUMP_ENTRIES entries, so it stays small
+ // regardless of the number of samples, and shows the truncation note.
+ REQUIRE(dflt.size() < 16 * 1024);
+ REQUIRE(dflt.find("more") != std::string::npos);
+
+ // The full dump contains an entry for every sample and is therefore much
+ // larger. It must not contain the truncation note.
+ REQUIRE(full.size() > num_samples);
+ REQUIRE(full.find("more") == std::string::npos);
+ REQUIRE(full.find("[" + std::to_string(num_samples - 1) + "]") != std::string::npos);
+}