Commit ec23637e for libheif
commit ec23637e41146527e72795a5bdbe08df18ba6b94
Author: riha ilyas <riha@research.itecz.au>
Date: Mon Oct 12 23:41:53 2026 +0530
Bound the sample index in the constant-size 'saiz' aux-info read
diff --git a/libheif/sequences/track.cc b/libheif/sequences/track.cc
index d2227cf8..7105e25e 100644
--- a/libheif/sequences/track.cc
+++ b/libheif/sequences/track.cc
@@ -169,6 +169,13 @@ Result<std::vector<uint8_t> > SampleAuxInfoReader::get_sample_info(const HeifFil
uint8_t size;
if (m_contiguous_and_constant_size) {
+ if (sample_idx >= m_saiz->get_num_samples()) {
+ // The 'saiz' describes aux info for fewer samples than the track contains;
+ // samples beyond its range carry none. The variable-size branch below already
+ // handles this via get_sample_size(); without the same bound here the offset
+ // runs past the aux-info chunk into an unrelated file region.
+ return {};
+ }
size = m_saiz->get_sample_size(0);
offset = m_singleChunk_offset + uint64_t{sample_idx} * size;
}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 37874f2c..ee3e59e9 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -145,6 +145,7 @@ add_libheif_test(sequence_timing_overflow)
add_libheif_test(sequence_vfr_durations)
add_libheif_test(track_reference_duplicates)
add_libheif_test(sequence_raw_sample_errors)
+add_libheif_test(sequence_short_aux_info)
add_libheif_test(sequence_nonvisual_sample_entry)
add_libheif_test(sequence_no_decoder_output)
add_libheif_test(tai)
diff --git a/tests/sequence_file_builder.h b/tests/sequence_file_builder.h
index d6ffc441..89ee734e 100644
--- a/tests/sequence_file_builder.h
+++ b/tests/sequence_file_builder.h
@@ -110,6 +110,8 @@ struct SampleAuxInfo
const char* type = "suid"; // aux_info_type: 'suid' = GIMI content ID, 'stai' = TAI timestamp
std::vector<uint8_t> data; // per-sample payload, identical for all samples
bool offset_past_eof = false; // let 'saio' point beyond the end of the file
+ uint32_t sample_count = 0; // samples the 'saiz' describes; 0 -> p.num_samples. Set lower
+ // to make 'saiz' cover fewer samples than the track has.
};
struct SequenceFileParams
@@ -280,6 +282,10 @@ inline std::vector<uint8_t> build_sequence_file(const SequenceFileParams& p)
std::vector<uint8_t> stsz = box("stsz", stsz_payload);
// --- saiz: one per aux info, constant per-sample size
+ auto aux_sample_count = [&](const SampleAuxInfo& aux) {
+ return aux.sample_count ? aux.sample_count : p.num_samples;
+ };
+
std::vector<std::vector<uint8_t>> saiz_boxes;
for (const auto& aux : p.aux_infos) {
std::vector<uint8_t> saiz_payload;
@@ -287,7 +293,7 @@ inline std::vector<uint8_t> build_sequence_file(const SequenceFileParams& p)
put_fourcc(saiz_payload, aux.type);
put32(saiz_payload, 0); // aux_info_type_parameter
saiz_payload.push_back(static_cast<uint8_t>(aux.data.size())); // default_sample_info_size
- put32(saiz_payload, p.num_samples); // sample_count
+ put32(saiz_payload, aux_sample_count(aux)); // sample_count
saiz_boxes.push_back(box("saiz", saiz_payload));
}
@@ -336,7 +342,7 @@ inline std::vector<uint8_t> build_sequence_file(const SequenceFileParams& p)
std::vector<size_t> aux_block_offsets; // relative to the start of the mdat payload
for (const auto& aux : p.aux_infos) {
aux_block_offsets.push_back(mdat_payload.size());
- for (uint32_t s = 0; s < p.num_samples; s++) {
+ for (uint32_t s = 0; s < aux_sample_count(aux); s++) {
append(mdat_payload, aux.data);
}
}
diff --git a/tests/sequence_short_aux_info.cc b/tests/sequence_short_aux_info.cc
new file mode 100644
index 00000000..cf699f6b
--- /dev/null
+++ b/tests/sequence_short_aux_info.cc
@@ -0,0 +1,129 @@
+/*
+ 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.
+*/
+
+// A 'saiz' box may describe aux info for fewer samples than the track contains
+// (only the opposite, more than the track, is rejected at load). Samples beyond
+// the 'saiz' range carry no aux info of that type. SampleAuxInfoReader::get_sample_info()
+// enforced that in its variable-size branch but not in the contiguous constant-size
+// branch, where it computed an offset for the undescribed sample and read
+// default_sample_info_size bytes from an unrelated file region (or failed on EOF).
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+#include "libheif/heif_sequences.h"
+#include "sequence_file_builder.h"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+using namespace seqfile;
+
+namespace {
+
+struct LoadedTrack
+{
+ heif_context* ctx = nullptr;
+ heif_track* track = nullptr;
+
+ ~LoadedTrack()
+ {
+ if (track) heif_track_release(track);
+ if (ctx) heif_context_free(ctx);
+ }
+};
+
+void load(const std::vector<uint8_t>& file, LoadedTrack& out)
+{
+ out.ctx = heif_context_alloc();
+ REQUIRE(out.ctx != nullptr);
+
+ heif_error err = heif_context_read_from_memory(out.ctx, file.data(), file.size(), nullptr);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(heif_context_has_sequence(out.ctx) == 1);
+
+ out.track = heif_context_get_track(out.ctx, 0);
+ REQUIRE(out.track != nullptr);
+}
+
+heif_raw_sequence_sample* next_sample(heif_track* track)
+{
+ heif_raw_sequence_sample* sample = nullptr;
+ heif_error err = heif_track_get_next_raw_sequence_sample(track, &sample);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(sample != nullptr);
+ return sample;
+}
+
+} // namespace
+
+
+TEST_CASE("samples beyond a short constant-size 'saiz' carry no content ID")
+{
+ // Two samples, but the 'saiz' describes only the first. The aux-info chunk in
+ // 'mdat' therefore holds a single content ID, directly followed by the end of
+ // the file. The second sample must read back as "no content ID", not as a read
+ // of the bytes past the aux-info chunk.
+ SequenceFileParams p;
+ p.mvhd_v1 = false;
+ p.mvhd_duration = 20;
+ p.mdhd_duration = 20;
+ p.with_editlist = false;
+ p.stts = {{2, 10}};
+ p.num_samples = 2;
+ p.sample_size = 4;
+
+ SampleAuxInfo aux;
+ aux.type = "suid";
+ aux.data = {'A', 0}; // utf8string "A"
+ aux.sample_count = 1; // 'saiz' covers sample 0 only
+ p.aux_infos = {aux};
+
+ LoadedTrack t;
+ load(build_sequence_file(p), t);
+
+ heif_raw_sequence_sample* s0 = next_sample(t.track);
+ const char* id0 = heif_raw_sequence_sample_get_gimi_sample_content_id(s0);
+ REQUIRE(id0 != nullptr);
+ REQUIRE(std::string(id0) == "A");
+ heif_string_release(id0);
+ heif_raw_sequence_sample_release(s0);
+
+ // The second sample is not described by the 'saiz'. Before the fix this read
+ // past the one-entry aux-info chunk and failed with heif_error_Invalid_input;
+ // next_sample() requires a clean heif_error_Ok. Its content ID is empty.
+ heif_raw_sequence_sample* s1 = next_sample(t.track);
+ const char* id1 = heif_raw_sequence_sample_get_gimi_sample_content_id(s1);
+ REQUIRE(id1 != nullptr);
+ REQUIRE(std::string(id1).empty());
+ heif_string_release(id1);
+ heif_raw_sequence_sample_release(s1);
+
+ heif_raw_sequence_sample* sample = nullptr;
+ heif_error err = heif_track_get_next_raw_sequence_sample(t.track, &sample);
+ REQUIRE(err.code == heif_error_End_of_sequence);
+ REQUIRE(sample == nullptr);
+}