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