Commit 3d9c246a for libheif
commit 3d9c246a778249d0dc6581a407f83433bcad6ca6
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Sat Sep 5 20:26:54 2026 +0200
Fix raw sequence sample leak and unguarded C entry point (GHSA-4rv4-953r-p24q)
Track::get_next_sample_raw_data() allocated the heif_raw_sequence_sample with a
raw 'new' and copied the whole sample payload into it before reading the sample
auxiliary information. Four error returns in that stage (a 'saio' offset past
EOF, a 'suid' content ID without NUL terminator or with an embedded NUL, and a
'stai' TAI payload that is not 9 bytes) dropped the pointer. The Result<T*>
error variant cannot carry it, so the C caller never received anything it could
release. Because nothing requires distinct 'trak' boxes to reference distinct
media bytes, a small file with many tracks aliasing one payload range leaked
that payload once per track and per call, and the surviving copy was not charged
to max_total_memory either (only the function-local DataExtent's handle was).
Hold the sample in a unique_ptr until the single success exit and move the
payload out of the extent instead of copying it, which also halves the peak
memory needed per sample.
The same entry point, heif_track_get_next_raw_sequence_sample(), was not wrapped
in exception_guard(), unlike its sibling heif_track_decode_next_image(). Its body
allocates a buffer of the file-controlled 'stsz' sample size, so a failing
allocation unwound std::bad_alloc across the extern "C" boundary and aborted the
host process. Wrap the shared implementation so that the public wrapper and a
future public options-taking variant are both covered (same class as
GHSA-7p2q-crf9-xm46).
Tests: move the hand-built 'urim' sequence-file builder out of
sequence_timing_overflow.cc into the shared tests/sequence_file_builder.h and
extend it with 'saiz'/'saio' sample auxiliary information. The new
sequence_raw_sample_errors test exercises each of the four error paths (clean
heif_error, output pointer untouched, call repeatable), a well-formed control
that checks payload, content ID and TAI timestamp survive the move, and an
oversized-sample case. Under LeakSanitizer the error-path cases report the leak
against the previous code at track.cc:1230/1231 and are clean with this fix.
diff --git a/libheif/api/libheif/heif_sequences.cc b/libheif/api/libheif/heif_sequences.cc
index bc2373e9..4ab489e2 100644
--- a/libheif/api/libheif/heif_sequences.cc
+++ b/libheif/api/libheif/heif_sequences.cc
@@ -285,28 +285,36 @@ static heif_error heif_track_get_next_raw_sequence_sample2(heif_track* track_ptr
heif_raw_sequence_sample** out_sample,
const heif_decoding_options* options)
{
- if (out_sample == nullptr) {
- return heif_error_null_pointer_argument;
- }
+ // The body allocates a buffer of the file-controlled 'stsz' sample size. An
+ // out-of-memory condition must come back as heif_error_out_of_memory instead of
+ // letting std::bad_alloc unwind across the extern "C" boundary and abort the host
+ // process (GHSA-4rv4-953r-p24q, same class as GHSA-7p2q-crf9-xm46). The guard sits
+ // here so that both the public wrapper below and a future public *2 variant are
+ // covered.
+ return exception_guard([&]() -> heif_error {
+ if (out_sample == nullptr) {
+ return heif_error_null_pointer_argument;
+ }
- auto track = track_ptr->track;
+ auto track = track_ptr->track;
- // `options` may be null (the no-options public wrapper below passes nullptr).
- // get_next_sample_raw_data() treats null as "apply the edit list" and only reads
- // options->ignore_sequence_editlist when options is non-null.
- //
- // We intentionally do not pre-check end_of_sequence_reached() here: that helper
- // compares against the edit-list-applied output count, whereas
- // get_next_sample_raw_data() applies ignore_sequence_editlist itself and signals
- // heif_error_End_of_sequence at the correct (option-dependent) boundary.
- auto decodingResult = track->get_next_sample_raw_data(options);
- if (!decodingResult) {
- return decodingResult.error_struct(track_ptr->context.get());
- }
+ // `options` may be null (the no-options public wrapper below passes nullptr).
+ // get_next_sample_raw_data() treats null as "apply the edit list" and only reads
+ // options->ignore_sequence_editlist when options is non-null.
+ //
+ // We intentionally do not pre-check end_of_sequence_reached() here: that helper
+ // compares against the edit-list-applied output count, whereas
+ // get_next_sample_raw_data() applies ignore_sequence_editlist itself and signals
+ // heif_error_End_of_sequence at the correct (option-dependent) boundary.
+ auto decodingResult = track->get_next_sample_raw_data(options);
+ if (!decodingResult) {
+ return decodingResult.error_struct(track_ptr->context.get());
+ }
- *out_sample = *decodingResult;
+ *out_sample = *decodingResult;
- return heif_error_success;
+ return heif_error_success;
+ });
}
diff --git a/libheif/sequences/track.cc b/libheif/sequences/track.cc
index 016f1218..61102f9c 100644
--- a/libheif/sequences/track.cc
+++ b/libheif/sequences/track.cc
@@ -28,6 +28,7 @@
#include "api_structs.h"
#include <algorithm>
#include <limits>
+#include <memory>
#include <utility>
@@ -1227,8 +1228,17 @@ Result<heif_raw_sequence_sample*> Track::get_next_sample_raw_data(const heif_dec
return readResult.error();
}
- heif_raw_sequence_sample* sample = new heif_raw_sequence_sample();
- sample->data = **readResult;
+ // Keep the sample in a unique_ptr until the single success exit below. Several
+ // error returns follow (sample auxiliary info driven by file bytes), and the
+ // Result<T*> error variant cannot carry the pointer back to the caller, so a raw
+ // 'new' here leaked the sample together with its full payload copy on every one
+ // of them (GHSA-4rv4-953r-p24q).
+ auto sample = std::make_unique<heif_raw_sequence_sample>();
+
+ // Move the payload out of the function-local extent instead of copying it. The
+ // extent is destroyed when this function returns anyway, and moving halves the
+ // peak memory needed per sample.
+ sample->data = std::move(**readResult);
// read sample duration
@@ -1273,7 +1283,7 @@ Result<heif_raw_sequence_sample*> Track::get_next_sample_raw_data(const heif_dec
m_next_sample_to_be_output++;
- return sample;
+ return sample.release();
}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index b8562b65..6dd22790 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -99,6 +99,7 @@ add_libheif_test(region)
add_libheif_test(sequence_no_track)
add_libheif_test(sequence_null_options)
add_libheif_test(sequence_timing_overflow)
+add_libheif_test(sequence_raw_sample_errors)
add_libheif_test(tai)
add_libheif_test(text)
add_libheif_test(cxx_wrapper)
diff --git a/tests/sequence_file_builder.h b/tests/sequence_file_builder.h
new file mode 100644
index 00000000..8ed566c4
--- /dev/null
+++ b/tests/sequence_file_builder.h
@@ -0,0 +1,348 @@
+/*
+ libheif integration tests: hand-built HEIF sequence files.
+
+ 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.
+*/
+
+// Builds minimal, structurally valid HEIF sequence files containing a single
+// 'urim' metadata track, following the box tree that Track::load() requires.
+// A metadata track is used so that the raw-sample API path can be exercised
+// without any codec plugin. Shared by the sequence_* tests.
+
+#ifndef LIBHEIF_TESTS_SEQUENCE_FILE_BUILDER_H
+#define LIBHEIF_TESTS_SEQUENCE_FILE_BUILDER_H
+
+#include <cstdint>
+#include <initializer_list>
+#include <limits>
+#include <vector>
+
+namespace seqfile {
+
+inline void put16(std::vector<uint8_t>& v, uint16_t x)
+{
+ v.push_back(static_cast<uint8_t>(x >> 8));
+ v.push_back(static_cast<uint8_t>(x));
+}
+
+inline void put32(std::vector<uint8_t>& v, uint32_t x)
+{
+ v.push_back(static_cast<uint8_t>(x >> 24));
+ v.push_back(static_cast<uint8_t>(x >> 16));
+ v.push_back(static_cast<uint8_t>(x >> 8));
+ v.push_back(static_cast<uint8_t>(x));
+}
+
+inline void put64(std::vector<uint8_t>& v, uint64_t x)
+{
+ for (int i = 7; i >= 0; i--) {
+ v.push_back(static_cast<uint8_t>(x >> (i * 8)));
+ }
+}
+
+inline void put_fourcc(std::vector<uint8_t>& v, const char* s)
+{
+ v.push_back(static_cast<uint8_t>(s[0]));
+ v.push_back(static_cast<uint8_t>(s[1]));
+ v.push_back(static_cast<uint8_t>(s[2]));
+ v.push_back(static_cast<uint8_t>(s[3]));
+}
+
+inline void append(std::vector<uint8_t>& dst, const std::vector<uint8_t>& src)
+{
+ dst.insert(dst.end(), src.begin(), src.end());
+}
+
+// Wrap a payload in a box: [uint32 size][fourcc type][payload].
+inline std::vector<uint8_t> box(const char* type, const std::vector<uint8_t>& payload)
+{
+ std::vector<uint8_t> b;
+ put32(b, static_cast<uint32_t>(8 + payload.size()));
+ put_fourcc(b, type);
+ append(b, payload);
+ return b;
+}
+
+inline std::vector<uint8_t> concat(std::initializer_list<std::vector<uint8_t>> parts)
+{
+ std::vector<uint8_t> out;
+ for (const auto& p : parts) {
+ append(out, p);
+ }
+ return out;
+}
+
+struct Stts_entry
+{
+ uint32_t sample_count;
+ uint32_t sample_delta;
+};
+
+// One 'saiz'/'saio' pair attached to the track. Every sample carries the same
+// aux-info payload (constant size, at most 255 bytes, because 'saiz' stores the
+// default size in a uint8_t). The payloads are appended to 'mdat' behind the
+// sample data.
+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
+};
+
+struct SequenceFileParams
+{
+ bool mvhd_v1 = true;
+ uint64_t mvhd_duration = std::numeric_limits<uint64_t>::max(); // indefinite sentinel
+ uint32_t timescale = 1000;
+
+ uint64_t mdhd_duration = 1;
+
+ bool with_editlist = true;
+ uint64_t elst_segment_duration = 1; // must equal mdhd_duration to match the repeat pattern
+
+ std::vector<Stts_entry> stts = {{1, 1}};
+
+ uint32_t num_samples = 1;
+ uint32_t sample_size = 1;
+
+ std::vector<SampleAuxInfo> aux_infos;
+};
+
+inline std::vector<uint8_t> build_sequence_file(const SequenceFileParams& p)
+{
+ // --- ftyp
+ std::vector<uint8_t> ftyp_payload;
+ put_fourcc(ftyp_payload, "msf1"); // major brand: HEIF image sequence
+ put32(ftyp_payload, 0); // minor version
+ put_fourcc(ftyp_payload, "msf1");
+ put_fourcc(ftyp_payload, "isom");
+ std::vector<uint8_t> ftyp = box("ftyp", ftyp_payload);
+
+ // --- mvhd
+ std::vector<uint8_t> mvhd_payload;
+ if (p.mvhd_v1) {
+ put32(mvhd_payload, 0x01000000); // version 1, flags 0
+ put64(mvhd_payload, 0); // creation_time
+ put64(mvhd_payload, 0); // modification_time
+ put32(mvhd_payload, p.timescale);
+ put64(mvhd_payload, p.mvhd_duration);
+ }
+ else {
+ put32(mvhd_payload, 0x00000000); // version 0, flags 0
+ put32(mvhd_payload, 0); // creation_time
+ put32(mvhd_payload, 0); // modification_time
+ put32(mvhd_payload, p.timescale);
+ put32(mvhd_payload, static_cast<uint32_t>(p.mvhd_duration));
+ }
+ put32(mvhd_payload, 0x00010000); // rate 1.0
+ put16(mvhd_payload, 0x0100); // volume
+ put16(mvhd_payload, 0); // reserved
+ put32(mvhd_payload, 0); // reserved
+ put32(mvhd_payload, 0); // reserved
+ for (int i = 0; i < 9; i++) put32(mvhd_payload, 0); // matrix
+ for (int i = 0; i < 6; i++) put32(mvhd_payload, 0); // pre_defined
+ put32(mvhd_payload, 2); // next_track_ID
+ std::vector<uint8_t> mvhd = box("mvhd", mvhd_payload);
+
+ // --- tkhd (version 1)
+ std::vector<uint8_t> tkhd_payload;
+ put32(tkhd_payload, 0x01000007); // version 1, flags = enabled|in_movie|in_preview
+ put64(tkhd_payload, 0); // creation_time
+ put64(tkhd_payload, 0); // modification_time
+ put32(tkhd_payload, 1); // track_ID
+ put32(tkhd_payload, 0); // reserved
+ put64(tkhd_payload, 0); // duration
+ put64(tkhd_payload, 0); // reserved
+ put16(tkhd_payload, 0); // layer
+ put16(tkhd_payload, 0); // alternate_group
+ put16(tkhd_payload, 0); // volume
+ put16(tkhd_payload, 0); // reserved
+ for (int i = 0; i < 9; i++) put32(tkhd_payload, 0); // matrix
+ put32(tkhd_payload, 0); // width
+ put32(tkhd_payload, 0); // height
+ std::vector<uint8_t> tkhd = box("tkhd", tkhd_payload);
+
+ // --- edts / elst (version 1, flags = repeat)
+ std::vector<uint8_t> edts;
+ if (p.with_editlist) {
+ std::vector<uint8_t> elst_payload;
+ put32(elst_payload, 0x01000001); // version 1, flags = Repeat_EditList
+ put32(elst_payload, 1); // entry_count
+ put64(elst_payload, p.elst_segment_duration);
+ put64(elst_payload, 0); // media_time
+ put16(elst_payload, 1); // media_rate_integer
+ put16(elst_payload, 0); // media_rate_fraction
+ edts = box("edts", box("elst", elst_payload));
+ }
+
+ // --- mdhd (version 0)
+ std::vector<uint8_t> mdhd_payload;
+ put32(mdhd_payload, 0x00000000); // version 0, flags 0
+ put32(mdhd_payload, 0); // creation_time
+ put32(mdhd_payload, 0); // modification_time
+ put32(mdhd_payload, p.timescale);
+ put32(mdhd_payload, static_cast<uint32_t>(p.mdhd_duration));
+ put16(mdhd_payload, 0x55c4); // language ('und')
+ put16(mdhd_payload, 0); // pre_defined
+ std::vector<uint8_t> mdhd = box("mdhd", mdhd_payload);
+
+ // --- hdlr (handler type 'meta')
+ std::vector<uint8_t> hdlr_payload;
+ put32(hdlr_payload, 0x00000000); // version 0, flags 0
+ put32(hdlr_payload, 0); // pre_defined
+ put_fourcc(hdlr_payload, "meta");
+ put32(hdlr_payload, 0); // reserved
+ put32(hdlr_payload, 0); // reserved
+ put32(hdlr_payload, 0); // reserved
+ hdlr_payload.push_back(0); // name (empty, null-terminated)
+ std::vector<uint8_t> hdlr = box("hdlr", hdlr_payload);
+
+ // --- nmhd (null media header for metadata tracks)
+ std::vector<uint8_t> nmhd_payload;
+ put32(nmhd_payload, 0x00000000); // version 0, flags 0
+ std::vector<uint8_t> nmhd = box("nmhd", nmhd_payload);
+
+ // --- stsd with a single 'urim' (URI meta) sample entry
+ std::vector<uint8_t> urim_payload;
+ for (int i = 0; i < 6; i++) urim_payload.push_back(0); // SampleEntry reserved
+ put16(urim_payload, 1); // data_reference_index
+ std::vector<uint8_t> urim = box("urim", urim_payload);
+
+ std::vector<uint8_t> stsd_payload;
+ put32(stsd_payload, 0x00000000); // version 0, flags 0
+ put32(stsd_payload, 1); // entry_count
+ append(stsd_payload, urim);
+ std::vector<uint8_t> stsd = box("stsd", stsd_payload);
+
+ // --- stts
+ std::vector<uint8_t> stts_payload;
+ put32(stts_payload, 0x00000000); // version 0, flags 0
+ put32(stts_payload, static_cast<uint32_t>(p.stts.size()));
+ for (const auto& e : p.stts) {
+ put32(stts_payload, e.sample_count);
+ put32(stts_payload, e.sample_delta);
+ }
+ std::vector<uint8_t> stts = box("stts", stts_payload);
+
+ // --- stsc: all samples in a single chunk
+ std::vector<uint8_t> stsc_payload;
+ put32(stsc_payload, 0x00000000); // version 0, flags 0
+ put32(stsc_payload, 1); // entry_count
+ put32(stsc_payload, 1); // first_chunk
+ put32(stsc_payload, p.num_samples); // samples_per_chunk
+ put32(stsc_payload, 1); // sample_description_index
+ std::vector<uint8_t> stsc = box("stsc", stsc_payload);
+
+ // --- stsz: fixed sample size
+ std::vector<uint8_t> stsz_payload;
+ put32(stsz_payload, 0x00000000); // version 0, flags 0
+ put32(stsz_payload, p.sample_size); // fixed sample size (non-zero -> no per-sample array)
+ put32(stsz_payload, p.num_samples); // sample_count
+ std::vector<uint8_t> stsz = box("stsz", stsz_payload);
+
+ // --- saiz: one per aux info, constant per-sample size
+ std::vector<std::vector<uint8_t>> saiz_boxes;
+ for (const auto& aux : p.aux_infos) {
+ std::vector<uint8_t> saiz_payload;
+ put32(saiz_payload, 0x00000001); // version 0, flags = aux_info_type present
+ 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
+ saiz_boxes.push_back(box("saiz", saiz_payload));
+ }
+
+ // --- stco / saio: offsets are patched below to point into the mdat payload
+ auto make_stco = [](uint32_t offset) {
+ std::vector<uint8_t> stco_payload;
+ put32(stco_payload, 0x00000000); // version 0, flags 0
+ put32(stco_payload, 1); // entry_count
+ put32(stco_payload, offset); // chunk offset
+ return box("stco", stco_payload);
+ };
+
+ auto make_saio = [](const SampleAuxInfo& aux, uint32_t offset) {
+ std::vector<uint8_t> saio_payload;
+ put32(saio_payload, 0x00000001); // version 0, flags = aux_info_type present
+ put_fourcc(saio_payload, aux.type);
+ put32(saio_payload, 0); // aux_info_type_parameter
+ put32(saio_payload, 1); // entry_count (single chunk)
+ put32(saio_payload, offset);
+ return box("saio", saio_payload);
+ };
+
+ auto assemble = [&](const std::vector<uint8_t>& stco,
+ const std::vector<std::vector<uint8_t>>& saio_boxes) {
+ std::vector<uint8_t> stbl_payload = concat({stsd, stts, stsc, stsz, stco});
+ for (const auto& b : saiz_boxes) append(stbl_payload, b);
+ for (const auto& b : saio_boxes) append(stbl_payload, b);
+ std::vector<uint8_t> stbl = box("stbl", stbl_payload);
+ std::vector<uint8_t> minf = box("minf", concat({nmhd, stbl}));
+ std::vector<uint8_t> mdia = box("mdia", concat({mdhd, hdlr, minf}));
+ std::vector<uint8_t> trak = box("trak", concat({tkhd, edts, mdia}));
+ std::vector<uint8_t> moov = box("moov", concat({mvhd, trak}));
+ return moov;
+ };
+
+ // --- mdat payload: num_samples * sample_size bytes of arbitrary content,
+ // followed by one block of aux-info payloads per saiz/saio pair.
+ std::vector<uint8_t> mdat_payload(static_cast<size_t>(p.num_samples) * p.sample_size);
+ for (size_t i = 0; i < mdat_payload.size(); i++) {
+ mdat_payload[i] = static_cast<uint8_t>(0xA0 + (i & 0x0f));
+ }
+
+ 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++) {
+ append(mdat_payload, aux.data);
+ }
+ }
+
+ // Box sizes are independent of the offset values, so we can size the file with
+ // placeholders, then patch the real offsets in one pass.
+ std::vector<std::vector<uint8_t>> saio_placeholders;
+ for (const auto& aux : p.aux_infos) {
+ saio_placeholders.push_back(make_saio(aux, 0));
+ }
+ std::vector<uint8_t> moov0 = assemble(make_stco(0), saio_placeholders);
+ uint32_t mdat_payload_offset = static_cast<uint32_t>(ftyp.size() + moov0.size() + 8 /* mdat header */);
+ uint32_t file_size = static_cast<uint32_t>(mdat_payload_offset + mdat_payload.size());
+
+ std::vector<std::vector<uint8_t>> saio_boxes;
+ for (size_t i = 0; i < p.aux_infos.size(); i++) {
+ uint32_t offset = p.aux_infos[i].offset_past_eof
+ ? file_size + 4096
+ : mdat_payload_offset + static_cast<uint32_t>(aux_block_offsets[i]);
+ saio_boxes.push_back(make_saio(p.aux_infos[i], offset));
+ }
+ std::vector<uint8_t> moov = assemble(make_stco(mdat_payload_offset), saio_boxes);
+
+ std::vector<uint8_t> mdat = box("mdat", mdat_payload);
+
+ return concat({ftyp, moov, mdat});
+}
+
+} // namespace seqfile
+
+#endif
diff --git a/tests/sequence_raw_sample_errors.cc b/tests/sequence_raw_sample_errors.cc
new file mode 100644
index 00000000..7405cbb8
--- /dev/null
+++ b/tests/sequence_raw_sample_errors.cc
@@ -0,0 +1,238 @@
+/*
+ libheif integration tests for the error paths of the raw sequence sample API.
+
+ 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.
+*/
+
+// Regression tests for GHSA-4rv4-953r-p24q.
+//
+// Track::get_next_sample_raw_data() allocated the heif_raw_sequence_sample, holding
+// a full copy of the sample payload, before reading the sample auxiliary
+// information ('saiz'/'saio'). Four error returns in that aux-info stage dropped
+// the pointer: the Result<T*> error variant cannot carry it, so the caller never
+// received anything it could release. A file with many tracks aliasing one payload
+// range leaked that payload once per track and per call. The same C entry point
+// also lacked exception_guard(), so a failing allocation of the file-controlled
+// sample size aborted the host process instead of returning heif_error_out_of_memory.
+//
+// The leak itself is only visible under LeakSanitizer. These tests pin down the
+// caller-visible contract on each of the four error paths (a clean heif_error,
+// *out_sample untouched, the call repeatable) and exercise the success path so the
+// payload move in the fix is covered. Run this binary under ASan/LSan to check the
+// allocation side: before the fix, every repetition in expect_clean_failure()
+// leaked one sample object plus one payload copy.
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+#include "libheif/heif_sequences.h"
+#include "libheif/heif_tai_timestamps.h"
+#include "sequence_file_builder.h"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+using namespace seqfile;
+
+namespace {
+
+// One 'urim' metadata track with a single 64-byte sample and no edit list.
+SequenceFileParams base_params()
+{
+ SequenceFileParams p;
+ p.mvhd_v1 = false;
+ p.mvhd_duration = 10;
+ p.mdhd_duration = 10;
+ p.with_editlist = false;
+ p.stts = {{1, 10}};
+ p.num_samples = 1;
+ p.sample_size = 64;
+ return p;
+}
+
+struct LoadedTrack
+{
+ heif_context* ctx = nullptr;
+ heif_track* track = nullptr;
+
+ LoadedTrack() = default;
+ LoadedTrack(const LoadedTrack&) = delete;
+ LoadedTrack& operator=(const LoadedTrack&) = delete;
+
+ ~LoadedTrack()
+ {
+ if (track) heif_track_release(track);
+ if (ctx) heif_context_free(ctx);
+ }
+};
+
+// Read the file and fetch its first track. Loading must succeed: all the malformed
+// aux-info variants below are only detected when the sample itself is read.
+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);
+}
+
+// Every call on a malformed file must fail with a clean error and leave the output
+// pointer untouched. The sample index does not advance on error, so repeating the
+// call exercises the same path again (and, before the fix, leaked again).
+void expect_clean_failure(heif_track* track, int repetitions = 20)
+{
+ for (int i = 0; i < repetitions; i++) {
+ heif_raw_sequence_sample* sample = nullptr;
+ heif_error err = heif_track_get_next_raw_sequence_sample(track, &sample);
+ REQUIRE(err.code == heif_error_Invalid_input);
+ REQUIRE(sample == nullptr);
+ }
+}
+
+} // namespace
+
+
+TEST_CASE("raw sample with non-terminated 'suid' content ID fails cleanly")
+{
+ SequenceFileParams p = base_params();
+ p.aux_infos = {{"suid", {'A'}}}; // utf8string without NUL terminator
+
+ LoadedTrack t;
+ load(build_sequence_file(p), t);
+ expect_clean_failure(t.track);
+}
+
+
+TEST_CASE("raw sample with embedded NUL in 'suid' content ID fails cleanly")
+{
+ SequenceFileParams p = base_params();
+ p.aux_infos = {{"suid", {'A', 0, 'B', 0}}};
+
+ LoadedTrack t;
+ load(build_sequence_file(p), t);
+ expect_clean_failure(t.track);
+}
+
+
+TEST_CASE("raw sample with 'saio' offset past end of file fails cleanly")
+{
+ SequenceFileParams p = base_params();
+ SampleAuxInfo aux;
+ aux.type = "suid";
+ aux.data = {'A', 0};
+ aux.offset_past_eof = true;
+ p.aux_infos = {aux};
+
+ LoadedTrack t;
+ load(build_sequence_file(p), t);
+ expect_clean_failure(t.track);
+}
+
+
+TEST_CASE("raw sample with wrong-sized 'stai' TAI timestamp fails cleanly")
+{
+ SequenceFileParams p = base_params();
+ p.aux_infos = {{"stai", {1, 2, 3, 4, 5}}}; // a TAI timestamp payload must be exactly 9 bytes
+
+ LoadedTrack t;
+ load(build_sequence_file(p), t);
+ expect_clean_failure(t.track);
+}
+
+
+TEST_CASE("raw sample with well-formed 'suid' and 'stai' aux info succeeds")
+{
+ // Control: the same file structure with valid aux-info payloads must deliver the
+ // sample together with its content ID and TAI timestamp. This also covers the
+ // payload move in Track::get_next_sample_raw_data(): the sample bytes must arrive
+ // intact.
+ SequenceFileParams p = base_params();
+ p.aux_infos = {
+ {"suid", {'u', 'r', 'n', ':', 'x', 0}},
+ {"stai", {0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x00}}
+ };
+
+ LoadedTrack t;
+ load(build_sequence_file(p), t);
+
+ heif_raw_sequence_sample* sample = nullptr;
+ heif_error err = heif_track_get_next_raw_sequence_sample(t.track, &sample);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(sample != nullptr);
+
+ size_t size = 0;
+ const uint8_t* data = heif_raw_sequence_sample_get_data(sample, &size);
+ REQUIRE(data != nullptr);
+ REQUIRE(size == 64);
+ for (size_t i = 0; i < size; i++) {
+ REQUIRE(data[i] == static_cast<uint8_t>(0xA0 + (i & 0x0f))); // pattern written by the builder
+ }
+ REQUIRE(heif_raw_sequence_sample_get_duration(sample) == 10);
+
+ const char* content_id = heif_raw_sequence_sample_get_gimi_sample_content_id(sample);
+ REQUIRE(content_id != nullptr);
+ REQUIRE(std::string(content_id) == "urn:x");
+ heif_string_release(content_id);
+
+ REQUIRE(heif_raw_sequence_sample_has_tai_timestamp(sample) == 1);
+ const heif_tai_timestamp_packet* tai = heif_raw_sequence_sample_get_tai_timestamp(sample);
+ REQUIRE(tai != nullptr);
+ REQUIRE(tai->tai_timestamp == 0x12345678);
+
+ heif_raw_sequence_sample_release(sample);
+
+ sample = nullptr;
+ err = heif_track_get_next_raw_sequence_sample(t.track, &sample);
+ REQUIRE(err.code == heif_error_End_of_sequence);
+ REQUIRE(sample == nullptr);
+}
+
+
+TEST_CASE("oversized raw sample is rejected with a heif_error")
+{
+ // The sample buffer is sized from the file's 'stsz'. A size above
+ // max_memory_block_size must come back as a heif_error through the C API. (The
+ // entry point is additionally wrapped in exception_guard() so that a failing
+ // allocation below that limit is reported as heif_error_out_of_memory instead of
+ // aborting the process; that path needs an rlimit and is not unit-tested here.)
+ SequenceFileParams p = base_params();
+ p.sample_size = 4096;
+
+ LoadedTrack t;
+ load(build_sequence_file(p), t);
+
+ // Tighten the limit only after loading so that the file read itself is unaffected.
+ heif_security_limits* limits = heif_context_get_security_limits(t.ctx);
+ limits->max_memory_block_size = 1024;
+
+ heif_raw_sequence_sample* sample = nullptr;
+ heif_error err = heif_track_get_next_raw_sequence_sample(t.track, &sample);
+ REQUIRE(err.code == heif_error_Memory_allocation_error);
+ REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded);
+ REQUIRE(sample == nullptr);
+}
diff --git a/tests/sequence_timing_overflow.cc b/tests/sequence_timing_overflow.cc
index ddcbaecc..b98ee5b0 100644
--- a/tests/sequence_timing_overflow.cc
+++ b/tests/sequence_timing_overflow.cc
@@ -34,260 +34,20 @@
// (max_sequence_frames) was also never applied to the amplified logical count
// (variant V6).
//
-// These tests build minimal HEIF sequence files by hand. A 'urim' metadata
+// These tests use the hand-built sequence files from sequence_file_builder.h. A 'urim' metadata
// track is used so the raw-sample API path exercises the timing table without
// needing any codec plugin.
#include "catch_amalgamated.hpp"
#include "libheif/heif.h"
#include "libheif/heif_sequences.h"
+#include "sequence_file_builder.h"
#include <cstdint>
#include <limits>
#include <vector>
-namespace {
-
-void put16(std::vector<uint8_t>& v, uint16_t x)
-{
- v.push_back(static_cast<uint8_t>(x >> 8));
- v.push_back(static_cast<uint8_t>(x));
-}
-
-void put32(std::vector<uint8_t>& v, uint32_t x)
-{
- v.push_back(static_cast<uint8_t>(x >> 24));
- v.push_back(static_cast<uint8_t>(x >> 16));
- v.push_back(static_cast<uint8_t>(x >> 8));
- v.push_back(static_cast<uint8_t>(x));
-}
-
-void put64(std::vector<uint8_t>& v, uint64_t x)
-{
- for (int i = 7; i >= 0; i--) {
- v.push_back(static_cast<uint8_t>(x >> (i * 8)));
- }
-}
-
-void put_fourcc(std::vector<uint8_t>& v, const char* s)
-{
- v.push_back(static_cast<uint8_t>(s[0]));
- v.push_back(static_cast<uint8_t>(s[1]));
- v.push_back(static_cast<uint8_t>(s[2]));
- v.push_back(static_cast<uint8_t>(s[3]));
-}
-
-// Wrap a payload in a box: [uint32 size][fourcc type][payload].
-std::vector<uint8_t> box(const char* type, const std::vector<uint8_t>& payload)
-{
- std::vector<uint8_t> b;
- put32(b, static_cast<uint32_t>(8 + payload.size()));
- put_fourcc(b, type);
- b.insert(b.end(), payload.begin(), payload.end());
- return b;
-}
-
-std::vector<uint8_t> concat(std::initializer_list<std::vector<uint8_t>> parts)
-{
- std::vector<uint8_t> out;
- for (const auto& p : parts) {
- out.insert(out.end(), p.begin(), p.end());
- }
- return out;
-}
-
-struct Stts_entry
-{
- uint32_t sample_count;
- uint32_t sample_delta;
-};
-
-struct SequenceFileParams
-{
- bool mvhd_v1 = true;
- uint64_t mvhd_duration = std::numeric_limits<uint64_t>::max(); // indefinite sentinel
- uint32_t timescale = 1000;
-
- uint64_t mdhd_duration = 1;
-
- bool with_editlist = true;
- uint64_t elst_segment_duration = 1; // must equal mdhd_duration to match the repeat pattern
-
- std::vector<Stts_entry> stts = {{1, 1}};
-
- uint32_t num_samples = 1;
- uint32_t sample_size = 1;
-};
-
-// Build a minimal, structurally valid HEIF sequence file containing a single
-// 'urim' metadata track, following the box tree that Track::load() requires.
-std::vector<uint8_t> build_sequence_file(const SequenceFileParams& p)
-{
- // --- ftyp
- std::vector<uint8_t> ftyp_payload;
- put_fourcc(ftyp_payload, "msf1"); // major brand: HEIF image sequence
- put32(ftyp_payload, 0); // minor version
- put_fourcc(ftyp_payload, "msf1");
- put_fourcc(ftyp_payload, "isom");
- std::vector<uint8_t> ftyp = box("ftyp", ftyp_payload);
-
- // --- mvhd
- std::vector<uint8_t> mvhd_payload;
- if (p.mvhd_v1) {
- put32(mvhd_payload, 0x01000000); // version 1, flags 0
- put64(mvhd_payload, 0); // creation_time
- put64(mvhd_payload, 0); // modification_time
- put32(mvhd_payload, p.timescale);
- put64(mvhd_payload, p.mvhd_duration);
- }
- else {
- put32(mvhd_payload, 0x00000000); // version 0, flags 0
- put32(mvhd_payload, 0); // creation_time
- put32(mvhd_payload, 0); // modification_time
- put32(mvhd_payload, p.timescale);
- put32(mvhd_payload, static_cast<uint32_t>(p.mvhd_duration));
- }
- put32(mvhd_payload, 0x00010000); // rate 1.0
- put16(mvhd_payload, 0x0100); // volume
- put16(mvhd_payload, 0); // reserved
- put32(mvhd_payload, 0); // reserved
- put32(mvhd_payload, 0); // reserved
- for (int i = 0; i < 9; i++) put32(mvhd_payload, 0); // matrix
- for (int i = 0; i < 6; i++) put32(mvhd_payload, 0); // pre_defined
- put32(mvhd_payload, 2); // next_track_ID
- std::vector<uint8_t> mvhd = box("mvhd", mvhd_payload);
-
- // --- tkhd (version 1)
- std::vector<uint8_t> tkhd_payload;
- put32(tkhd_payload, 0x01000007); // version 1, flags = enabled|in_movie|in_preview
- put64(tkhd_payload, 0); // creation_time
- put64(tkhd_payload, 0); // modification_time
- put32(tkhd_payload, 1); // track_ID
- put32(tkhd_payload, 0); // reserved
- put64(tkhd_payload, 0); // duration
- put64(tkhd_payload, 0); // reserved
- put16(tkhd_payload, 0); // layer
- put16(tkhd_payload, 0); // alternate_group
- put16(tkhd_payload, 0); // volume
- put16(tkhd_payload, 0); // reserved
- for (int i = 0; i < 9; i++) put32(tkhd_payload, 0); // matrix
- put32(tkhd_payload, 0); // width
- put32(tkhd_payload, 0); // height
- std::vector<uint8_t> tkhd = box("tkhd", tkhd_payload);
-
- // --- edts / elst (version 1, flags = repeat)
- std::vector<uint8_t> edts;
- if (p.with_editlist) {
- std::vector<uint8_t> elst_payload;
- put32(elst_payload, 0x01000001); // version 1, flags = Repeat_EditList
- put32(elst_payload, 1); // entry_count
- put64(elst_payload, p.elst_segment_duration);
- put64(elst_payload, 0); // media_time
- put16(elst_payload, 1); // media_rate_integer
- put16(elst_payload, 0); // media_rate_fraction
- edts = box("edts", box("elst", elst_payload));
- }
-
- // --- mdhd (version 0)
- std::vector<uint8_t> mdhd_payload;
- put32(mdhd_payload, 0x00000000); // version 0, flags 0
- put32(mdhd_payload, 0); // creation_time
- put32(mdhd_payload, 0); // modification_time
- put32(mdhd_payload, p.timescale);
- put32(mdhd_payload, static_cast<uint32_t>(p.mdhd_duration));
- put16(mdhd_payload, 0x55c4); // language ('und')
- put16(mdhd_payload, 0); // pre_defined
- std::vector<uint8_t> mdhd = box("mdhd", mdhd_payload);
-
- // --- hdlr (handler type 'meta')
- std::vector<uint8_t> hdlr_payload;
- put32(hdlr_payload, 0x00000000); // version 0, flags 0
- put32(hdlr_payload, 0); // pre_defined
- put_fourcc(hdlr_payload, "meta");
- put32(hdlr_payload, 0); // reserved
- put32(hdlr_payload, 0); // reserved
- put32(hdlr_payload, 0); // reserved
- hdlr_payload.push_back(0); // name (empty, null-terminated)
- std::vector<uint8_t> hdlr = box("hdlr", hdlr_payload);
-
- // --- nmhd (null media header for metadata tracks)
- std::vector<uint8_t> nmhd_payload;
- put32(nmhd_payload, 0x00000000); // version 0, flags 0
- std::vector<uint8_t> nmhd = box("nmhd", nmhd_payload);
-
- // --- stsd with a single 'urim' (URI meta) sample entry
- std::vector<uint8_t> urim_payload;
- for (int i = 0; i < 6; i++) urim_payload.push_back(0); // SampleEntry reserved
- put16(urim_payload, 1); // data_reference_index
- std::vector<uint8_t> urim = box("urim", urim_payload);
-
- std::vector<uint8_t> stsd_payload;
- put32(stsd_payload, 0x00000000); // version 0, flags 0
- put32(stsd_payload, 1); // entry_count
- stsd_payload.insert(stsd_payload.end(), urim.begin(), urim.end());
- std::vector<uint8_t> stsd = box("stsd", stsd_payload);
-
- // --- stts
- std::vector<uint8_t> stts_payload;
- put32(stts_payload, 0x00000000); // version 0, flags 0
- put32(stts_payload, static_cast<uint32_t>(p.stts.size()));
- for (const auto& e : p.stts) {
- put32(stts_payload, e.sample_count);
- put32(stts_payload, e.sample_delta);
- }
- std::vector<uint8_t> stts = box("stts", stts_payload);
-
- // --- stsc: all samples in a single chunk
- std::vector<uint8_t> stsc_payload;
- put32(stsc_payload, 0x00000000); // version 0, flags 0
- put32(stsc_payload, 1); // entry_count
- put32(stsc_payload, 1); // first_chunk
- put32(stsc_payload, p.num_samples); // samples_per_chunk
- put32(stsc_payload, 1); // sample_description_index
- std::vector<uint8_t> stsc = box("stsc", stsc_payload);
-
- // --- stsz: fixed sample size
- std::vector<uint8_t> stsz_payload;
- put32(stsz_payload, 0x00000000); // version 0, flags 0
- put32(stsz_payload, p.sample_size); // fixed sample size (non-zero -> no per-sample array)
- put32(stsz_payload, p.num_samples); // sample_count
- std::vector<uint8_t> stsz = box("stsz", stsz_payload);
-
- // --- stco: single chunk offset, patched below to point at the mdat payload
- auto make_stco = [](uint32_t offset) {
- std::vector<uint8_t> stco_payload;
- put32(stco_payload, 0x00000000); // version 0, flags 0
- put32(stco_payload, 1); // entry_count
- put32(stco_payload, offset); // chunk offset
- return box("stco", stco_payload);
- };
-
- auto assemble = [&](const std::vector<uint8_t>& stco) {
- std::vector<uint8_t> stbl = box("stbl", concat({stsd, stts, stsc, stsz, stco}));
- std::vector<uint8_t> minf = box("minf", concat({nmhd, stbl}));
- std::vector<uint8_t> mdia = box("mdia", concat({mdhd, hdlr, minf}));
- std::vector<uint8_t> trak = box("trak", concat({tkhd, edts, mdia}));
- std::vector<uint8_t> moov = box("moov", concat({mvhd, trak}));
- return moov;
- };
-
- // Box sizes are independent of the offset value, so we can size the file with a
- // placeholder, then patch the real offset in one pass.
- std::vector<uint8_t> moov0 = assemble(make_stco(0));
- uint32_t mdat_payload_offset = static_cast<uint32_t>(ftyp.size() + moov0.size() + 8 /* mdat header */);
- std::vector<uint8_t> moov = assemble(make_stco(mdat_payload_offset));
-
- // --- mdat: num_samples * sample_size bytes of arbitrary content
- std::vector<uint8_t> mdat_payload(static_cast<size_t>(p.num_samples) * p.sample_size);
- for (size_t i = 0; i < mdat_payload.size(); i++) {
- mdat_payload[i] = static_cast<uint8_t>(0xA0 + (i & 0x0f));
- }
- std::vector<uint8_t> mdat = box("mdat", mdat_payload);
-
- return concat({ftyp, moov, mdat});
-}
-
-} // namespace
+using namespace seqfile;
TEST_CASE("repeat edit list with indefinite duration terminates")