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