Commit a1645b6f for libheif

commit a1645b6f13dad2fd9132532fa890f4fd11de38d0
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Mon Oct 5 01:19:01 2026 +0200

    Document the lifetime of heif_error.message and copy it in the heif-enc loaders (GHSA-hvwh-xpj6-ph5x)

    For an error with a detailed message, the string that heif_error.message
    points to is stored in the object on which the failing function was
    called: the heif_context, the image item behind a heif_image_handle, the
    heif_image, or the context of a heif_track. It is replaced by the next
    error on that object and freed together with it. The public header only
    said that the pointer is never NULL, so code that released the object
    first and read the message afterwards read freed memory
    (GHSA-hvwh-xpj6-ph5x).

    The library itself does not do that, but two input loaders of heif-enc
    did: the WebP loader (since v1.22.1) and the raw loader (since v1.22.0)
    released their heif_image when a plane could not be allocated and returned
    the error of that call, whose message heif-enc then printed. They now copy
    the message first, with the stable_error() helper that the HEIF loader
    already had and that moves to heifio/decoder.h.

    Document the lifetime at heif_error::message. In heif-dec and heif-info,
    print the message before the image handle is released. This was not a
    use after free, since the image item stays alive inside the context, but
    it did not follow the rule as it is documented now.

diff --git a/examples/heif_dec.cc b/examples/heif_dec.cc
index a14471d9..ef73bd1e 100644
--- a/examples/heif_dec.cc
+++ b/examples/heif_dec.cc
@@ -337,8 +337,8 @@ int decode_single_image(heif_image_handle* handle,
                                 encoder->chroma(false, depth_bit_depth),
                                 nullptr);
         if (err.code) {
-          heif_image_handle_release(depth_handle);
           std::cerr << "Could not decode depth image: " << err.message << "\n";
+          heif_image_handle_release(depth_handle);
           return 1;
         }

@@ -392,17 +392,17 @@ int decode_single_image(heif_image_handle* handle,
                                   encoder->chroma(false, aux_bit_depth),
                                   nullptr);
           if (err.code) {
-            heif_image_handle_release(aux_handle);
             std::cerr << "Could not decode auxiliary image: " << err.message << "\n";
+            heif_image_handle_release(aux_handle);
             return 1;
           }

           const char* auxTypeC = nullptr;
           err = heif_image_handle_get_auxiliary_type(aux_handle, &auxTypeC);
           if (err.code) {
+            std::cerr << "Could not get type of auxiliary image: " << err.message << "\n";
             heif_image_release(aux_image);
             heif_image_handle_release(aux_handle);
-            std::cerr << "Could not get type of auxiliary image: " << err.message << "\n";
             return 1;
           }

diff --git a/examples/heif_info.cc b/examples/heif_info.cc
index a2332fb8..08004ae0 100644
--- a/examples/heif_info.cc
+++ b/examples/heif_info.cc
@@ -991,8 +991,8 @@ int main(int argc, char** argv)
   heif_image* image;
   err = heif_decode_image(handle, &image, heif_colorspace_undefined, heif_chroma_undefined, NULL);
   if (err.code != 0) {
-    heif_image_handle_release(handle);
     std::cerr << "Could not decode primage image: " << err.message << "\n";
+    heif_image_handle_release(handle);
     return 1;
   }

diff --git a/heifio/decoder.h b/heifio/decoder.h
index 5e80d250..973e6b87 100644
--- a/heifio/decoder.h
+++ b/heifio/decoder.h
@@ -29,6 +29,7 @@

 #include "libheif/heif.h"
 #include <memory>
+#include <string>
 #include <vector>

 struct InputImage
@@ -39,4 +40,19 @@ struct InputImage
   heif_orientation orientation = heif_orientation_normal;
 };

+
+// libheif's heif_error.message is owned by the object whose function returned the error
+// (the heif_context, heif_image_handle or heif_image) and becomes invalid once that object
+// is released. A loader that releases the object and then returns the error has to copy
+// the message first (GHSA-hvwh-xpj6-ph5x). This copies it into a thread-local string, so
+// that it stays valid for the caller and concurrent callers on different threads do not
+// race on the buffer.
+// The caller is expected to log the error promptly (heif-enc calls exit() immediately).
+inline heif_error stable_error(const heif_error& err)
+{
+  static thread_local std::string msg;
+  msg = err.message ? err.message : "";
+  return {err.code, err.subcode, msg.c_str()};
+}
+
 #endif //LIBHEIF_DECODER_H
diff --git a/heifio/decoder_heif.cc b/heifio/decoder_heif.cc
index 4bea2164..cb65fadc 100644
--- a/heifio/decoder_heif.cc
+++ b/heifio/decoder_heif.cc
@@ -32,19 +32,6 @@

 static struct heif_error heif_error_ok = {heif_error_Ok, heif_suberror_Unspecified, "Success"};

-// libheif's heif_error.message is owned by the heif_context (or heif_image_handle)
-// and becomes invalid once the owning object is released. Copy the message into a
-// thread-local string so it stays valid for the caller after we have released the
-// context, and so concurrent callers on different threads do not race on the buffer.
-// The caller is expected to log the error promptly (heif-enc calls exit() immediately).
-static heif_error stable_error(const heif_error& err)
-{
-  static thread_local std::string msg;
-  msg = err.message ? err.message : "";
-  return {err.code, err.subcode, msg.c_str()};
-}
-
-
 heif_error loadHEIF(const char* filename, InputImage* input_image)
 {
   heif_context* ctx = heif_context_alloc();
diff --git a/heifio/decoder_raw.cc b/heifio/decoder_raw.cc
index a2ed77ae..047fb12c 100644
--- a/heifio/decoder_raw.cc
+++ b/heifio/decoder_raw.cc
@@ -191,8 +191,10 @@ heif_error loadRAW(const char* filename, const RawImageParameters& params, Input
                                  params.datatype, params.bit_depth,
                                  &component_idx);
   if (err.code != heif_error_Ok) {
+    // the message is stored in the image that we release
+    heif_error stable = stable_error(err);
     heif_image_release(image);
-    return err;
+    return stable;
   }

   // Get writable pointer and copy data row by row
diff --git a/heifio/decoder_webp.cc b/heifio/decoder_webp.cc
index c057977d..7106e113 100644
--- a/heifio/decoder_webp.cc
+++ b/heifio/decoder_webp.cc
@@ -192,9 +192,11 @@ heif_error loadWEBP(const char* filename, InputImage* input_image)
     image_ptr = std::shared_ptr<heif_image>(image,
       [](heif_image* img) { heif_image_release(img); });

+    // The message of an error of heif_image_add_plane() is stored in the image, which is
+    // released when we return.
     err = heif_image_add_plane(image, heif_channel_interleaved, (int)width, (int)height, 8);
     if (err.code)
-      return err;
+      return stable_error(err);
     size_t stride;
     uint8_t* ptr = heif_image_get_plane2(image, heif_channel_interleaved, &stride);
     // decode into heif image
@@ -241,19 +243,21 @@ heif_error loadWEBP(const char* filename, InputImage* input_image)
     uint8_t* ptr[4] = {};
     const int uv_width = (width + 1) / 2;
     const int uv_height = (height + 1) / 2;
+    // The message of an error of heif_image_add_plane() is stored in the image, which is
+    // released when we return.
     err = heif_image_add_plane(image, heif_channel_Y, (int)width, (int)height, 8);
     if (err.code)
-      return err;
+      return stable_error(err);
     err = heif_image_add_plane(image, heif_channel_Cb, uv_width, uv_height, 8);
     if (err.code)
-      return err;
+      return stable_error(err);
     err = heif_image_add_plane(image, heif_channel_Cr, uv_width, uv_height, 8);
     if (err.code)
-      return err;
+      return stable_error(err);
     if (config.input.has_alpha) {
       err = heif_image_add_plane(image, heif_channel_Alpha, (int)width, (int)height, 8);
       if (err.code)
-        return err;
+        return stable_error(err);
     }
     ptr[0] = heif_image_get_plane2(image, heif_channel_Y, &stride[0]);
     ptr[1] = heif_image_get_plane2(image, heif_channel_Cb, &stride[1]);
diff --git a/libheif/api/libheif/heif_error.h b/libheif/api/libheif/heif_error.h
index 06565f02..dca7b1d5 100644
--- a/libheif/api/libheif/heif_error.h
+++ b/libheif/api/libheif/heif_error.h
@@ -297,6 +297,13 @@ typedef struct heif_error
   heif_suberror_code subcode;

   // textual error message (is always defined, you do not have to check for NULL)
+  //
+  // The string is owned by libheif. For many errors, it is stored in the object on which the
+  // function that returned the error was called (the heif_context, heif_image_handle,
+  // heif_image, heif_track, ...). It is only valid until the next function is called on that
+  // object, and at most until that object or the heif_context it belongs to is released.
+  // If you need the message for longer, copy it. In particular, read or copy it before you
+  // free the heif_context or release the image whose function returned the error.
   const char* message;
 } heif_error;