Commit 6b6757b4 for libheif

commit 6b6757b469ecec8eb6644f6f530515975ed5f8c1
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Mon Aug 24 19:10:49 2026 +0200

    catch C++ exceptions at C API read/decode entry points (GHSA-7p2q-crf9-xm46)

diff --git a/libheif/api/libheif/heif_context.cc b/libheif/api/libheif/heif_context.cc
index b25ab494..58ed347b 100644
--- a/libheif/api/libheif/heif_context.cc
+++ b/libheif/api/libheif/heif_context.cc
@@ -57,22 +57,28 @@ void heif_context_free(heif_context* ctx)
 heif_error heif_context_read_from_file(heif_context* ctx, const char* filename,
                                        const heif_reading_options*)
 {
-  Error err = ctx->context->read_from_file(filename);
-  return err.error_struct(ctx->context.get());
+  return exception_guard([&]() -> heif_error {
+    Error err = ctx->context->read_from_file(filename);
+    return err.error_struct(ctx->context.get());
+  });
 }

 heif_error heif_context_read_from_memory(heif_context* ctx, const void* mem, size_t size,
                                          const heif_reading_options*)
 {
-  Error err = ctx->context->read_from_memory(mem, size, true);
-  return err.error_struct(ctx->context.get());
+  return exception_guard([&]() -> heif_error {
+    Error err = ctx->context->read_from_memory(mem, size, true);
+    return err.error_struct(ctx->context.get());
+  });
 }

 heif_error heif_context_read_from_memory_without_copy(heif_context* ctx, const void* mem, size_t size,
                                                       const heif_reading_options*)
 {
-  Error err = ctx->context->read_from_memory(mem, size, false);
-  return err.error_struct(ctx->context.get());
+  return exception_guard([&]() -> heif_error {
+    Error err = ctx->context->read_from_memory(mem, size, false);
+    return err.error_struct(ctx->context.get());
+  });
 }

 heif_error heif_context_read_from_reader(heif_context* ctx,
@@ -80,10 +86,12 @@ heif_error heif_context_read_from_reader(heif_context* ctx,
                                          void* userdata,
                                          const heif_reading_options*)
 {
-  auto reader = std::make_shared<StreamReader_CApi>(reader_func_table, userdata);
+  return exception_guard([&]() -> heif_error {
+    auto reader = std::make_shared<StreamReader_CApi>(reader_func_table, userdata);

-  Error err = ctx->context->read(reader);
-  return err.error_struct(ctx->context.get());
+    Error err = ctx->context->read(reader);
+    return err.error_struct(ctx->context.get());
+  });
 }

 // TODO: heif_error heif_context_read_from_file_descriptor(heif_context*, int fd);
diff --git a/libheif/api/libheif/heif_decoding.cc b/libheif/api/libheif/heif_decoding.cc
index 2b845816..45ebbaef 100644
--- a/libheif/api/libheif/heif_decoding.cc
+++ b/libheif/api/libheif/heif_decoding.cc
@@ -249,26 +249,29 @@ heif_error heif_decode_image(const heif_image_handle* in_handle,
   }

   *out_img = nullptr;
-  heif_item_id id = in_handle->image->get_id();

-  heif_decoding_options dec_options;
-  fill_default_decoding_options(dec_options);
-  heif_decoding_options_copy(&dec_options, input_options);
+  return exception_guard([&]() -> heif_error {
+    heif_item_id id = in_handle->image->get_id();

-  Result<std::shared_ptr<HeifPixelImage> > decodingResult = in_handle->context->decode_image(id,
-                                                                                             colorspace,
-                                                                                             chroma,
-                                                                                             dec_options,
-                                                                                             false, 0, 0, {});
+    heif_decoding_options dec_options;
+    fill_default_decoding_options(dec_options);
+    heif_decoding_options_copy(&dec_options, input_options);

-  if (!decodingResult) {
-    return decodingResult.error_struct(in_handle->image.get());
-  }
+    Result<std::shared_ptr<HeifPixelImage> > decodingResult = in_handle->context->decode_image(id,
+                                                                                               colorspace,
+                                                                                               chroma,
+                                                                                               dec_options,
+                                                                                               false, 0, 0, {});
+
+    if (!decodingResult) {
+      return decodingResult.error_struct(in_handle->image.get());
+    }

-  std::shared_ptr<HeifPixelImage> img = *decodingResult;
+    std::shared_ptr<HeifPixelImage> img = *decodingResult;

-  *out_img = new heif_image();
-  (*out_img)->image = std::move(img);
+    *out_img = new heif_image();
+    (*out_img)->image = std::move(img);

-  return Error::Ok.error_struct(in_handle->image.get());
+    return Error::Ok.error_struct(in_handle->image.get());
+  });
 }
diff --git a/libheif/api/libheif/heif_sequences.cc b/libheif/api/libheif/heif_sequences.cc
index e628adff..9b822242 100644
--- a/libheif/api/libheif/heif_sequences.cc
+++ b/libheif/api/libheif/heif_sequences.cc
@@ -171,54 +171,56 @@ heif_error heif_track_decode_next_image(heif_track* track_ptr,
     return heif_error_null_pointer_argument;
   }

-  // --- get the visual track
+  return exception_guard([&]() -> heif_error {
+    // --- get the visual track

-  auto track = track_ptr->track;
+    auto track = track_ptr->track;

-  // --- reached end of sequence ?
+    // --- reached end of sequence ?

-  if (track->end_of_sequence_reached()) {
-    *out_img = nullptr;
-    return {heif_error_End_of_sequence, heif_suberror_Unspecified, "End of sequence"};
-  }
+    if (track->end_of_sequence_reached()) {
+      *out_img = nullptr;
+      return {heif_error_End_of_sequence, heif_suberror_Unspecified, "End of sequence"};
+    }

-  // --- decode next sequence image
+    // --- decode next sequence image

-  std::unique_ptr<heif_decoding_options, void(*)(heif_decoding_options*)> opts(heif_decoding_options_alloc(), heif_decoding_options_free);
-  heif_decoding_options_copy(opts.get(), options);
+    std::unique_ptr<heif_decoding_options, void(*)(heif_decoding_options*)> opts(heif_decoding_options_alloc(), heif_decoding_options_free);
+    heif_decoding_options_copy(opts.get(), options);


-  auto visual_track = std::dynamic_pointer_cast<Track_Visual>(track);
-  if (!visual_track) {
-    return {
-      heif_error_Usage_error,
-      heif_suberror_Invalid_parameter_value,
-      "Cannot get image from non-visual track."
-    };
-  }
+    auto visual_track = std::dynamic_pointer_cast<Track_Visual>(track);
+    if (!visual_track) {
+      return {
+        heif_error_Usage_error,
+        heif_suberror_Invalid_parameter_value,
+        "Cannot get image from non-visual track."
+      };
+    }

-  auto decodingResult = visual_track->decode_next_image_sample(*opts);
-  if (!decodingResult) {
-    return decodingResult.error_struct(track_ptr->context.get());
-  }
+    auto decodingResult = visual_track->decode_next_image_sample(*opts);
+    if (!decodingResult) {
+      return decodingResult.error_struct(track_ptr->context.get());
+    }

-  std::shared_ptr<HeifPixelImage> img = *decodingResult;
+    std::shared_ptr<HeifPixelImage> img = *decodingResult;


-  // --- convert to output colorspace
+    // --- convert to output colorspace

-  auto conversion_result = track_ptr->context->convert_to_output_colorspace(img, colorspace, chroma, *opts);
-  if (!conversion_result) {
-    return conversion_result.error_struct(track_ptr->context.get());
-  }
-  else {
-    img = *conversion_result;
-  }
+    auto conversion_result = track_ptr->context->convert_to_output_colorspace(img, colorspace, chroma, *opts);
+    if (!conversion_result) {
+      return conversion_result.error_struct(track_ptr->context.get());
+    }
+    else {
+      img = *conversion_result;
+    }

-  *out_img = new heif_image();
-  (*out_img)->image = std::move(img);
+    *out_img = new heif_image();
+    (*out_img)->image = std::move(img);

-  return {};
+    return {};
+  });
 }


diff --git a/libheif/api/libheif/heif_tiling.cc b/libheif/api/libheif/heif_tiling.cc
index 9dbd6e53..66e41559 100644
--- a/libheif/api/libheif/heif_tiling.cc
+++ b/libheif/api/libheif/heif_tiling.cc
@@ -107,29 +107,32 @@ heif_error heif_image_handle_decode_image_tile(const heif_image_handle* in_handl
     return heif_error_null_pointer_argument;
   }

-  heif_item_id id = in_handle->image->get_id();
-
-  heif_decoding_options* dec_options = heif_decoding_options_alloc();
-  heif_decoding_options_copy(dec_options, input_options);
-
-  Result<std::shared_ptr<HeifPixelImage> > decodingResult = in_handle->context->decode_image(id,
-                                                                                             colorspace,
-                                                                                             chroma,
-                                                                                             *dec_options,
-                                                                                             true, x0, y0,
-                                                                                             {});
-  heif_decoding_options_free(dec_options);
-
-  if (!decodingResult) {
-    return decodingResult.error_struct(in_handle->image.get());
-  }
+  return exception_guard([&]() -> heif_error {
+    heif_item_id id = in_handle->image->get_id();
+
+    // RAII so the options are freed even if decode_image() throws.
+    std::unique_ptr<heif_decoding_options, void(*)(heif_decoding_options*)>
+        dec_options(heif_decoding_options_alloc(), heif_decoding_options_free);
+    heif_decoding_options_copy(dec_options.get(), input_options);
+
+    Result<std::shared_ptr<HeifPixelImage> > decodingResult = in_handle->context->decode_image(id,
+                                                                                               colorspace,
+                                                                                               chroma,
+                                                                                               *dec_options,
+                                                                                               true, x0, y0,
+                                                                                               {});
+
+    if (!decodingResult) {
+      return decodingResult.error_struct(in_handle->image.get());
+    }

-  std::shared_ptr<HeifPixelImage> img = *decodingResult;
+    std::shared_ptr<HeifPixelImage> img = *decodingResult;

-  *out_img = new heif_image();
-  (*out_img)->image = std::move(img);
+    *out_img = new heif_image();
+    (*out_img)->image = std::move(img);

-  return Error::Ok.error_struct(in_handle->image.get());
+    return Error::Ok.error_struct(in_handle->image.get());
+  });
 }


diff --git a/libheif/api_structs.h b/libheif/api_structs.h
index 6277ae3c..b8ceb2d9 100644
--- a/libheif/api_structs.h
+++ b/libheif/api_structs.h
@@ -23,12 +23,43 @@

 #include "image/pixelimage.h"
 #include "context.h"
+#include "error.h"

 #include <memory>
 #include <vector>
 #include <string>
+#include <exception>
+#include <new>
+#include <stdexcept>
 #include "image-items/image_item.h"

+
+// Run a C API entry point body and translate any escaping C++ exception into a
+// heif_error. No exception may cross the extern "C" boundary (that is undefined
+// behaviour for C callers), and an out-of-memory condition must not abort the
+// process (GHSA-7p2q-crf9-xm46). The returned errors have string-literal messages,
+// so reporting them never touches the allocator that is already failing.
+template <typename F>
+static inline heif_error exception_guard(F&& body) noexcept
+{
+  try {
+    return body();
+  }
+  catch (const std::bad_alloc&) {
+    return heif_error_out_of_memory;
+  }
+  catch (const std::length_error&) {
+    // e.g. a std::vector/std::string asked to exceed max_size()
+    return heif_error_out_of_memory;
+  }
+  catch (const std::exception&) {
+    return heif_error_internal_exception;
+  }
+  catch (...) {
+    return heif_error_internal_exception;
+  }
+}
+
 struct heif_image_handle
 {
   std::shared_ptr<ImageItem> image;
diff --git a/libheif/error.cc b/libheif/error.cc
index 63228d14..0c7a9dfd 100644
--- a/libheif/error.cc
+++ b/libheif/error.cc
@@ -30,6 +30,18 @@ const heif_error heif_error_null_pointer_argument {
   "NULL argument passed"
 };

+const heif_error heif_error_out_of_memory {
+  heif_error_Memory_allocation_error,
+  heif_suberror_Unspecified,
+  "Out of memory"
+};
+
+const heif_error heif_error_internal_exception {
+  heif_error_Usage_error,
+  heif_suberror_Unspecified,
+  "Internal error (unexpected C++ exception)"
+};
+

 // static
 const char Error::kSuccess[] = "Success";
diff --git a/libheif/error.h b/libheif/error.h
index 0d6292d8..c66e5a1e 100644
--- a/libheif/error.h
+++ b/libheif/error.h
@@ -40,6 +40,11 @@

 extern const heif_error heif_error_null_pointer_argument;

+// Predefined errors with string-literal messages (static storage, no allocation).
+// Safe to return from a std::bad_alloc handler, where the allocator must not be used.
+extern const heif_error heif_error_out_of_memory;
+extern const heif_error heif_error_internal_exception;
+

 class ErrorBuffer
 {
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 80ed92ac..7d6c596f 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -49,6 +49,15 @@ else()
     add_libheif_test(avc_box)
     add_libheif_test(file_layout)
     add_libheif_test(image_description_metadata)
+    add_libheif_test(exception_guard)
+
+    # decompression_bomb builds the bomb payload with the library-internal
+    # compress_brotli(), so it needs both Brotli and full symbol visibility.
+    if (Brotli_FOUND)
+        add_libheif_test(decompression_bomb)
+    else()
+        message(INFO " Disabling the decompression bomb test because Brotli was not found")
+    endif ()
 endif()

 if (ENABLE_EXPERIMENTAL_FEATURES AND NOT WITH_REDUCED_VISIBILITY)
@@ -74,12 +83,6 @@ add_libheif_test(text)
 add_libheif_test(cxx_wrapper)
 add_libheif_test(component_descriptions)

-if (Brotli_FOUND)
-    add_libheif_test(decompression_bomb)
-else()
-    message(INFO " Disabling the decompression bomb test because Brotli was not found")
-endif ()
-
 if (WITH_OPENJPH_ENCODER AND SUPPORTS_J2K_HT_ENCODING)
     add_libheif_test(encode_htj2k)
 else()
diff --git a/tests/exception_guard.cc b/tests/exception_guard.cc
new file mode 100644
index 00000000..1b00ba42
--- /dev/null
+++ b/tests/exception_guard.cc
@@ -0,0 +1,85 @@
+/*
+  libheif unit tests
+
+  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.
+*/
+
+// Tests for exception_guard(), the translation layer that keeps C++ exceptions
+// from crossing the C API boundary. It converts an out-of-memory condition into
+// a clean heif_error instead of letting std::bad_alloc abort the process
+// (hardening for GHSA-7p2q-crf9-xm46 / GHSA-24wx-9w62-c96w).
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+#include "api_structs.h"
+
+#include <new>
+#include <stdexcept>
+#include <cstring>
+
+TEST_CASE("exception_guard passes through the body result unchanged") {
+  heif_error ok = exception_guard([]() -> heif_error {
+    return {heif_error_Ok, heif_suberror_Unspecified, "fine"};
+  });
+  REQUIRE(ok.code == heif_error_Ok);
+
+  heif_error custom = exception_guard([]() -> heif_error {
+    return {heif_error_Invalid_input, heif_suberror_End_of_data, "boom"};
+  });
+  REQUIRE(custom.code == heif_error_Invalid_input);
+  REQUIRE(custom.subcode == heif_suberror_End_of_data);
+}
+
+TEST_CASE("exception_guard converts std::bad_alloc to a memory-allocation error") {
+  heif_error err = exception_guard([]() -> heif_error {
+    throw std::bad_alloc();
+  });
+  REQUIRE(err.code == heif_error_Memory_allocation_error);
+  REQUIRE(err.message != nullptr);
+  // The message must be a static literal (returning it must not allocate).
+  REQUIRE(strlen(err.message) > 0);
+}
+
+TEST_CASE("exception_guard converts std::length_error to a memory-allocation error") {
+  // A std::vector/std::string asked to exceed max_size() throws std::length_error;
+  // treat it as an allocation failure rather than an abort.
+  heif_error err = exception_guard([]() -> heif_error {
+    throw std::length_error("too large");
+  });
+  REQUIRE(err.code == heif_error_Memory_allocation_error);
+}
+
+TEST_CASE("exception_guard converts other std::exceptions to an internal error") {
+  heif_error err = exception_guard([]() -> heif_error {
+    throw std::runtime_error("unexpected");
+  });
+  REQUIRE(err.code == heif_error_Usage_error);
+  REQUIRE(err.message != nullptr);
+}
+
+TEST_CASE("exception_guard converts non-std exceptions to an internal error") {
+  heif_error err = exception_guard([]() -> heif_error {
+    throw 42;
+  });
+  REQUIRE(err.code == heif_error_Usage_error);
+}