Commit a999ad0b for libheif
commit a999ad0b74658de50621fde444c43e91045fecbf
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon Aug 24 21:47:47 2026 +0200
reject 'stts'/'ctts' tables describing more samples than can exist
The per-entry sample counts are 32 bit and get_number_of_samples() summed them
into a uint32_t. The sum wraps, and since the only use of the result is the
consistency check against the sample count in 'stsz', a malformed table could
wrap back onto the expected value and pass a check meant to reject it.
The number of samples in a track is a 32-bit quantity: ISO/IEC 14496-12 8.7.3
defines the 'stsz' sample_count as "the number of samples in the track", and
'stts'/'ctts' are run-length tables over that same set of samples. A sum beyond
32 bits is therefore malformed input rather than a large but valid file, so the
table is rejected while parsing instead of widening the accessor.
The bound is max_sequence_frames, matching what 'stsz' already applies to its
sample count and what both boxes already apply to their entry_count. It falls
back to the 32-bit range when that limit is disabled.
Reported by the fuzzing build, whose 'integer' sanitizer flags the wrap
(seq_boxes.cc: "unsigned integer overflow: 2037408125 + 4294929769 cannot be
represented in type uint32_t"). It affected 11 inputs of the local corpus.
Box_stts had the identical bug and is fixed along with Box_ctts.
diff --git a/libheif/sequences/seq_boxes.cc b/libheif/sequences/seq_boxes.cc
index 69900856..6dc8cf21 100644
--- a/libheif/sequences/seq_boxes.cc
+++ b/libheif/sequences/seq_boxes.cc
@@ -587,6 +587,29 @@ Error Box_stts::parse(BitstreamRange& range, const heif_security_limits* limits)
m_entries[i] = entry;
}
+ // The run-lengths describe the samples of the track, so their sum is the track's
+ // sample count: a 32-bit quantity that 'stsz' caps at max_sequence_frames. Nothing
+ // above bounds the sum, so an oversized table would wrap get_number_of_samples()
+ // and could thereby slip past the consistency check against 'stsz'.
+ {
+ uint64_t total_samples = 0;
+ for (const auto& entry : m_entries) {
+ total_samples += entry.sample_count;
+ }
+
+ uint64_t max_samples = (limits->max_sequence_frames > 0
+ ? limits->max_sequence_frames
+ : uint64_t{0xFFFFFFFF});
+
+ if (total_samples > max_samples) {
+ return {
+ heif_error_Memory_allocation_error,
+ heif_suberror_Security_limit_exceeded,
+ "Security limit for maximum number of sequence frames exceeded"
+ };
+ }
+ }
+
return range.get_error();
}
@@ -664,12 +687,17 @@ uint64_t Box_stts::get_total_duration(bool include_last_frame_duration)
uint32_t Box_stts::get_number_of_samples() const
{
- uint32_t total = 0;
+ // The number of samples in a track is a 32-bit quantity (ISO/IEC 14496-12
+ // 8.7.3: the 'stsz' sample_count "gives the number of samples in the track"),
+ // and parse() rejects tables whose run-lengths sum beyond that. Accumulate in
+ // 64 bit anyway so that a box built by the encoder cannot wrap silently.
+ uint64_t total = 0;
for (const auto& entry : m_entries) {
total += entry.sample_count;
}
- return total;
+ assert(total <= 0xFFFFFFFF);
+ return static_cast<uint32_t>(total);
}
@@ -743,6 +771,29 @@ Error Box_ctts::parse(BitstreamRange& range, const heif_security_limits* limits)
m_entries[i] = entry;
}
+ // The run-lengths describe the samples of the track, so their sum is the track's
+ // sample count: a 32-bit quantity that 'stsz' caps at max_sequence_frames. Nothing
+ // above bounds the sum, so an oversized table would wrap get_number_of_samples()
+ // and could thereby slip past the consistency check against 'stsz'.
+ {
+ uint64_t total_samples = 0;
+ for (const auto& entry : m_entries) {
+ total_samples += entry.sample_count;
+ }
+
+ uint64_t max_samples = (limits->max_sequence_frames > 0
+ ? limits->max_sequence_frames
+ : uint64_t{0xFFFFFFFF});
+
+ if (total_samples > max_samples) {
+ return {
+ heif_error_Memory_allocation_error,
+ heif_suberror_Security_limit_exceeded,
+ "Security limit for maximum number of sequence frames exceeded"
+ };
+ }
+ }
+
return range.get_error();
}
@@ -772,12 +823,17 @@ int32_t Box_ctts::compute_min_offset() const
uint32_t Box_ctts::get_number_of_samples() const
{
- uint32_t total = 0;
+ // The number of samples in a track is a 32-bit quantity (ISO/IEC 14496-12
+ // 8.7.3: the 'stsz' sample_count "gives the number of samples in the track"),
+ // and parse() rejects tables whose run-lengths sum beyond that. Accumulate in
+ // 64 bit anyway so that a box built by the encoder cannot wrap silently.
+ uint64_t total = 0;
for (const auto& entry : m_entries) {
total += entry.sample_count;
}
- return total;
+ assert(total <= 0xFFFFFFFF);
+ return static_cast<uint32_t>(total);
}