Commit efdf1a6e for libheif

commit efdf1a6e61daf245aadabe172f1bcd25cb136957
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Mon Oct 26 11:09:38 2026 +0100

    Set the 'urim' URI output to NULL on error

    heif_track_get_urim_sample_entry_uri_of_first_cluster() now clears
    *out_uri on entry, so that a caller that misses the error check releases
    a NULL pointer instead of an uninitialised one. Add a test for both the
    error and the success path.

diff --git a/libheif/api/libheif/heif_sequences.cc b/libheif/api/libheif/heif_sequences.cc
index 4ab489e2..07e49dbc 100644
--- a/libheif/api/libheif/heif_sequences.cc
+++ b/libheif/api/libheif/heif_sequences.cc
@@ -238,6 +238,10 @@ uint32_t heif_track_get_sample_entry_type_of_first_cluster(const heif_track* tra

 heif_error heif_track_get_urim_sample_entry_uri_of_first_cluster(const heif_track* track, const char** out_uri)
 {
+  if (out_uri) {
+    *out_uri = nullptr;
+  }
+
   Result<std::string> uriResult = track->track->get_first_cluster_urim_uri();

   if (!uriResult) {
diff --git a/libheif/api/libheif/heif_sequences.h b/libheif/api/libheif/heif_sequences.h
index a04d4754..0a400ed6 100644
--- a/libheif/api/libheif/heif_sequences.h
+++ b/libheif/api/libheif/heif_sequences.h
@@ -244,6 +244,7 @@ uint32_t heif_track_get_sample_entry_type_of_first_cluster(const heif_track*);
  * Only call this for tracks with 'urim' sample entry types. It will return an error otherwise.
  *
  * @param out_uri A string with the URI will be returned. Free this string with `heif_string_release()`.
+ *                On error, this is set to NULL.
  */
 LIBHEIF_API
 heif_error heif_track_get_urim_sample_entry_uri_of_first_cluster(const heif_track*,
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index ee3e59e9..e921296f 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -148,6 +148,7 @@ 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(sequence_urim_missing_uri)
 add_libheif_test(tai)
 add_libheif_test(text)
 if (NOT WITH_REDUCED_VISIBILITY)
diff --git a/tests/sequence_urim_missing_uri.cc b/tests/sequence_urim_missing_uri.cc
new file mode 100644
index 00000000..074fb87a
--- /dev/null
+++ b/tests/sequence_urim_missing_uri.cc
@@ -0,0 +1,140 @@
+/*
+  libheif integration tests for reading the URI of a 'urim' sample entry.
+
+  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-r9cg-w542-g7hj.
+//
+// heif_track_get_urim_sample_entry_uri_of_first_cluster() wrote *out_uri only on
+// success. heif-dec -S and heif-info printed and released the output pointer even
+// after an error, so a 'urim' sample entry without 'uri ' child box made them use
+// an uninitialized pointer. The function now sets *out_uri to NULL on entry.
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+#include "libheif/heif_sequences.h"
+#include "sequence_file_builder.h"
+
+#include <cstdint>
+#include <cstring>
+#include <vector>
+
+using namespace seqfile;
+
+namespace {
+
+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;
+}
+
+// 'urim' sample entry with a 'uri ' child box holding the given URI.
+std::vector<uint8_t> urim_with_uri(const char* uri)
+{
+  std::vector<uint8_t> uri_payload;
+  put32(uri_payload, 0); // version, flags
+  uri_payload.insert(uri_payload.end(), uri, uri + strlen(uri) + 1);
+
+  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
+  append(urim_payload, box("uri ", uri_payload));
+  return box("urim", urim_payload);
+}
+
+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);
+  }
+};
+
+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);
+
+  out.track = heif_context_get_track(out.ctx, 0);
+  REQUIRE(out.track != nullptr);
+  REQUIRE(heif_track_get_sample_entry_type_of_first_cluster(out.track) == heif_fourcc('u', 'r', 'i', 'm'));
+}
+
+} // namespace
+
+
+TEST_CASE("'urim' without 'uri' box returns an error and a NULL URI")
+{
+  LoadedTrack t;
+  load(build_sequence_file(base_params()), t); // default sample entry: 'urim' without children
+
+  int dummy;
+  const char* uri = reinterpret_cast<const char*>(&dummy); // must be overwritten
+  heif_error err = heif_track_get_urim_sample_entry_uri_of_first_cluster(t.track, &uri);
+  REQUIRE(err.code == heif_error_Invalid_input);
+  REQUIRE(uri == nullptr);
+
+  // releasing the NULL output, as a careless caller would, is harmless
+  heif_string_release(uri);
+
+  // a NULL output argument is tolerated
+  err = heif_track_get_urim_sample_entry_uri_of_first_cluster(t.track, nullptr);
+  REQUIRE(err.code == heif_error_Invalid_input);
+}
+
+
+TEST_CASE("'urim' with 'uri' box returns the URI")
+{
+  SequenceFileParams p = base_params();
+  p.sample_entry = urim_with_uri("urn:example:test");
+
+  LoadedTrack t;
+  load(build_sequence_file(p), t);
+
+  const char* uri = nullptr;
+  heif_error err = heif_track_get_urim_sample_entry_uri_of_first_cluster(t.track, &uri);
+  REQUIRE(err.code == heif_error_Ok);
+  REQUIRE(uri != nullptr);
+  REQUIRE(strcmp(uri, "urn:example:test") == 0);
+  heif_string_release(uri);
+}