Commit 45a40ca1 for libheif

commit 45a40ca1579ac102b509eb64901ab58124b38cbc
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Sat Sep 19 16:19:31 2026 +0200

    Store the TAI timestamp by value in ImageDescription (GHSA-qwpf-5wf7-r996)

    ImageDescription owned a raw heif_tai_timestamp_packet* and declared only a
    destructor, so the implicitly-generated copy operations shallow-copied the
    pointer. ImageItem::encode_to_bitstream_and_boxes() assigns a sliced copy of
    the source image's ImageDescription; the temporary shared the pointer and
    freed it at the end of the full-expression, leaving both the item and the
    source image with a dangling pointer. This caused a use-after-free while
    generating the 'itai' property and a double free at teardown. It is reachable
    by encoding any image that carries a TAI timestamp, including transcoding a
    file with an 'itai' property.

    Store the timestamp as std::optional<heif_tai_timestamp_packet> by value so
    the compiler-generated copy/move operations duplicate it correctly. This
    removes the raw owning pointer, the explicit destructor, and all manual copy
    logic (Rule of Zero), matching how Box_itai already stores the same struct.

    Add a regression test covering the direct-API and transcode paths, and fix a
    pre-existing leak in the existing image-tai test that allocated an image
    before a possible SKIP.

diff --git a/libheif/image/image_description.cc b/libheif/image/image_description.cc
index 437a68b3..16e4289e 100644
--- a/libheif/image/image_description.cc
+++ b/libheif/image/image_description.cc
@@ -113,12 +113,6 @@ std::vector<uint32_t> map_cmpd_to_component_ids(const std::vector<uint32_t>& cmp
 }


-ImageDescription::~ImageDescription()
-{
-  heif_tai_timestamp_packet_release(m_tai_timestamp);
-}
-
-
 void ImageDescription::copy_metadata_from(const ImageDescription& other)
 {
   m_premultiplied_alpha = other.m_premultiplied_alpha;
@@ -133,12 +127,7 @@ void ImageDescription::copy_metadata_from(const ImageDescription& other)
   m_amve = other.m_amve;
   m_nominal_diffuse_white_luminance = other.m_nominal_diffuse_white_luminance;

-  heif_tai_timestamp_packet_release(m_tai_timestamp);
-  m_tai_timestamp = nullptr;
-  if (other.m_tai_timestamp) {
-    m_tai_timestamp = heif_tai_timestamp_packet_alloc();
-    heif_tai_timestamp_packet_copy(m_tai_timestamp, other.m_tai_timestamp);
-  }
+  m_tai_timestamp = other.m_tai_timestamp;

   m_gimi_sample_content_id = other.m_gimi_sample_content_id;

diff --git a/libheif/image/image_description.h b/libheif/image/image_description.h
index 8b89502c..cc9a4ff4 100644
--- a/libheif/image/image_description.h
+++ b/libheif/image/image_description.h
@@ -156,7 +156,7 @@ struct ComponentDescription
 class ImageDescription
 {
 public:
-  virtual ~ImageDescription();
+  virtual ~ImageDescription() = default;

   // TODO: Decide who is responsible for writing the colr boxes.
   //       Currently it is distributed over various places.
@@ -270,15 +270,17 @@ public:
   void unset_nominal_diffuse_white() { m_nominal_diffuse_white_luminance.reset(); }

   virtual Error set_tai_timestamp(const heif_tai_timestamp_packet* tai) {
-    delete m_tai_timestamp;
-
-    m_tai_timestamp = heif_tai_timestamp_packet_alloc();
-    heif_tai_timestamp_packet_copy(m_tai_timestamp, tai);
+    // Version-aware copy into a freshly initialized packet, mirroring
+    // heif_tai_timestamp_packet_alloc() + heif_tai_timestamp_packet_copy().
+    heif_tai_timestamp_packet packet{};
+    packet.version = 1;
+    heif_tai_timestamp_packet_copy(&packet, tai);
+    m_tai_timestamp = packet;
     return Error::Ok;
   }

   [[nodiscard]] const heif_tai_timestamp_packet* get_tai_timestamp() const {
-    return m_tai_timestamp;
+    return m_tai_timestamp ? &*m_tai_timestamp : nullptr;
   }

   // --- GIMI content ID
@@ -470,7 +472,9 @@ private:
   std::optional<heif_ambient_viewing_environment> m_amve;
   std::optional<uint32_t> m_nominal_diffuse_white_luminance;

-  heif_tai_timestamp_packet* m_tai_timestamp = nullptr;
+  // Stored by value so that the compiler-generated copy/move operations
+  // duplicate it correctly (GHSA-qwpf-5wf7-r996). std::nullopt == "no timestamp".
+  std::optional<heif_tai_timestamp_packet> m_tai_timestamp;

   // Empty string means "no content id assigned".
   std::string m_gimi_sample_content_id;
diff --git a/tests/tai.cc b/tests/tai.cc
index 7210a9f5..7bc5acf5 100644
--- a/tests/tai.cc
+++ b/tests/tai.cc
@@ -38,8 +38,10 @@ TEST_CASE( "image-tai" )

   std::string filename = get_tests_output_file_path("tai-1.heic");

-  heif_image* img = createImage_RGB_planar();
+  // Query the encoder first: get_encoder_or_skip_test() may abort the test
+  // via SKIP(), and anything allocated before it would then be leaked.
   heif_encoder* enc = get_encoder_or_skip_test(heif_compression_HEVC);
+  heif_image* img = createImage_RGB_planar();
   heif_context* ctx = heif_context_alloc();

   heif_image_handle* handle;
@@ -137,4 +139,153 @@ TEST_CASE( "image-tai" )
   REQUIRE(timestamp->timestamp_generation_failure == 0);
   REQUIRE(timestamp->timestamp_is_modified == 0);
   heif_tai_timestamp_packet_release(timestamp);
-}
\ No newline at end of file
+}
+
+// Pick a codec for which we have both an encoder and a decoder so that the
+// test can encode, read back, decode and re-encode.
+static heif_compression_format pick_roundtrip_format_or_skip()
+{
+  for (heif_compression_format format : {heif_compression_AV1,
+                                         heif_compression_HEVC,
+                                         heif_compression_uncompressed}) {
+    if (heif_have_encoder_for_format(format) && heif_have_decoder_for_format(format)) {
+      return format;
+    }
+  }
+
+  SKIP("No codec with both encoder and decoder available, skipping test");
+  return heif_compression_undefined;
+}
+
+
+static void require_tai_timestamp(heif_tai_timestamp_packet* tai, uint64_t expected)
+{
+  REQUIRE(tai != nullptr);
+  REQUIRE(tai->tai_timestamp == expected);
+  REQUIRE(tai->synchronization_state == 1);
+  REQUIRE(tai->timestamp_generation_failure == 0);
+  REQUIRE(tai->timestamp_is_modified == 1);
+  heif_tai_timestamp_packet_release(tai);
+}
+
+
+// Regression test for GHSA-qwpf-5wf7-r996.
+//
+// Encoding an image that carries a TAI timestamp copied the ImageDescription
+// of the source image into the ImageItem through a sliced temporary. The
+// temporary shared the raw heif_tai_timestamp_packet pointer with the source
+// image and freed it when it went out of scope, leaving both the item and
+// the source image with a dangling pointer (use-after-free while generating
+// the 'itai' property, double free at teardown).
+TEST_CASE( "image-tai-encode-keeps-timestamp" )
+{
+  heif_error err{};
+
+  err = heif_init(nullptr);
+  REQUIRE(err.code == heif_error_Ok);
+
+  heif_compression_format format = pick_roundtrip_format_or_skip();
+
+  const uint64_t expected_timestamp = 0x0123456789ABCDEFULL;
+
+  heif_image* img = createImage_RGB_planar();
+
+  heif_tai_timestamp_packet* tai = heif_tai_timestamp_packet_alloc();
+  tai->tai_timestamp = expected_timestamp;
+  tai->synchronization_state = 1;
+  tai->timestamp_generation_failure = 0;
+  tai->timestamp_is_modified = 1;
+  err = heif_image_set_tai_timestamp(img, tai);
+  REQUIRE(err.code == heif_error_Ok);
+  heif_tai_timestamp_packet_release(tai);
+
+  heif_context* ctx = heif_context_alloc();
+  heif_encoder* enc = nullptr;
+  err = heif_context_get_encoder_for_format(ctx, format, &enc);
+  REQUIRE(err.code == heif_error_Ok);
+
+  // --- encode: the timestamp is copied from the image into the item
+
+  heif_image_handle* handle = nullptr;
+  err = heif_context_encode_image(ctx, img, enc, nullptr, &handle);
+  REQUIRE(err.code == heif_error_Ok);
+
+  // the encoded item carries the timestamp as an 'itai' property
+
+  heif_item_id itemId = heif_image_handle_get_item_id(handle);
+  tai = nullptr;
+  err = heif_item_get_property_tai_timestamp(ctx, itemId, &tai);
+  REQUIRE(err.code == heif_error_Ok);
+  require_tai_timestamp(tai, expected_timestamp);
+
+  // the source image must still own its own, intact timestamp
+
+  tai = nullptr;
+  err = heif_image_get_tai_timestamp(img, &tai);
+  REQUIRE(err.code == heif_error_Ok);
+  require_tai_timestamp(tai, expected_timestamp);
+
+  // encoding the same image a second time must work as well
+
+  heif_image_handle* handle2 = nullptr;
+  err = heif_context_encode_image(ctx, img, enc, nullptr, &handle2);
+  REQUIRE(err.code == heif_error_Ok);
+  heif_image_handle_release(handle2);
+
+  std::string filename = get_tests_output_file_path("tai-transcode.heif");
+  err = heif_context_write_to_file(ctx, filename.c_str());
+  REQUIRE(err.code == heif_error_Ok);
+
+  heif_image_handle_release(handle);
+  heif_encoder_release(enc);
+  heif_context_free(ctx);
+  heif_image_release(img);
+
+
+  // --- transcode: decode the file and encode the decoded image again
+
+  ctx = heif_context_alloc();
+  err = heif_context_read_from_file(ctx, filename.c_str(), nullptr);
+  REQUIRE(err.code == heif_error_Ok);
+
+  err = heif_context_get_primary_image_handle(ctx, &handle);
+  REQUIRE(err.code == heif_error_Ok);
+
+  img = nullptr;
+  err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr);
+  REQUIRE(err.code == heif_error_Ok);
+
+  heif_image_handle_release(handle);
+  heif_context_free(ctx);
+
+  // the decoded image has the timestamp attached
+
+  tai = nullptr;
+  err = heif_image_get_tai_timestamp(img, &tai);
+  REQUIRE(err.code == heif_error_Ok);
+  require_tai_timestamp(tai, expected_timestamp);
+
+  ctx = heif_context_alloc();
+  err = heif_context_get_encoder_for_format(ctx, format, &enc);
+  REQUIRE(err.code == heif_error_Ok);
+
+  handle = nullptr;
+  err = heif_context_encode_image(ctx, img, enc, nullptr, &handle);
+  REQUIRE(err.code == heif_error_Ok);
+
+  itemId = heif_image_handle_get_item_id(handle);
+  tai = nullptr;
+  err = heif_item_get_property_tai_timestamp(ctx, itemId, &tai);
+  REQUIRE(err.code == heif_error_Ok);
+  require_tai_timestamp(tai, expected_timestamp);
+
+  tai = nullptr;
+  err = heif_image_get_tai_timestamp(img, &tai);
+  REQUIRE(err.code == heif_error_Ok);
+  require_tai_timestamp(tai, expected_timestamp);
+
+  heif_image_handle_release(handle);
+  heif_encoder_release(enc);
+  heif_context_free(ctx);
+  heif_image_release(img);
+}