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);
+}