Commit 0979e4a0 for libheif
commit 0979e4a0c83ca837c399feaa0f74d78eb0d4ed6f
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Sun Sep 20 01:38:58 2026 +0200
Split the GHSA-qfj5 regression test by symbol visibility
The uncompressed_interleaved_alpha_plane test was registered for every build
with the uncompressed codec, but its last test case drives the uncompressed
encoder factory with a HeifPixelImage assembled through internal calls. With
WITH_REDUCED_VISIBILITY=ON those symbols are not exported, so the test failed
to link on AppVeyor (MSVC) and on any other reduced-visibility build.
Move that test case into uncompressed_interleaved_alpha_plane_internal.cc,
registered only with full symbol visibility like the other internal tests.
The three public-API test cases, which are the primary regression coverage
for the fix, stay in the original file and keep building everywhere.
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index c1a22059..67160b76 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -77,6 +77,11 @@ endif()
if (NOT WITH_REDUCED_VISIBILITY AND WITH_UNCOMPRESSED_CODEC)
add_libheif_test(uncompressed_box)
+
+ # Companion of uncompressed_interleaved_alpha_plane below. It drives the
+ # uncompressed encoder factory with a HeifPixelImage assembled through
+ # internal calls, so it needs full symbol visibility.
+ add_libheif_test(uncompressed_interleaved_alpha_plane_internal)
endif()
# --- tests that only access the public API
diff --git a/tests/uncompressed_interleaved_alpha_plane.cc b/tests/uncompressed_interleaved_alpha_plane.cc
index 8e9315b4..ba373892 100644
--- a/tests/uncompressed_interleaved_alpha_plane.cc
+++ b/tests/uncompressed_interleaved_alpha_plane.cc
@@ -40,16 +40,17 @@
// valid configuration. It is now rejected when the plane is added, and the
// uncompressed encoder additionally refuses such an image should it be
// assembled by another route.
+//
+// This file only uses the public API and builds in every configuration. The
+// encoder-side check needs library-internal classes and is tested in
+// uncompressed_interleaved_alpha_plane_internal.cc, which only builds with full
+// symbol visibility.
#include "catch_amalgamated.hpp"
#include "libheif/heif.h"
-#include "api_structs.h"
-#include "image/pixelimage.h"
-#include "codecs/uncompressed/unc_encoder.h"
#include "test_utils.h"
#include <cstring>
-#include <memory>
namespace {
@@ -178,39 +179,3 @@ TEST_CASE("Planar RGB images still accept a separate alpha plane")
heif_image_release(image);
}
-
-// transfer_channel_from_image_as() moves a plane between images without going through
-// add_channel(). This is how a decoded alpha auxiliary image is attached to the main
-// image, so an interleaved image with a separate alpha plane can still be assembled
-// inside the library. The uncompressed encoder must refuse it instead of reading past
-// the end of the interleaved component list.
-TEST_CASE("Uncompressed encoder refuses an interleaved image carrying a separate alpha plane")
-{
- const heif_security_limits* limits = heif_get_global_security_limits();
-
- for (const auto& fmt : interleaved_formats) {
- INFO(fmt.name);
-
- auto image = std::make_shared<HeifPixelImage>();
- image->create(WIDTH, HEIGHT, heif_colorspace_RGB, fmt.chroma);
- REQUIRE(image->fill_new_channel(heif_channel_interleaved, 0x80, WIDTH, HEIGHT, fmt.bit_depth, limits).error_code == heif_error_Ok);
-
- auto alpha = std::make_shared<HeifPixelImage>();
- alpha->create(WIDTH, HEIGHT, heif_colorspace_monochrome, heif_chroma_monochrome);
- REQUIRE(alpha->fill_new_channel(heif_channel_Y, 0xFF, WIDTH, HEIGHT, fmt.bit_depth, limits).error_code == heif_error_Ok);
-
- REQUIRE(image->transfer_channel_from_image_as(alpha, heif_channel_Y, heif_channel_Alpha).error_code == heif_error_Ok);
- REQUIRE(image->has_channel(heif_channel_Alpha));
-
- // The encoder factory is where the interleaved encoders are instantiated.
- heif_encoding_options* options = heif_encoding_options_alloc();
- auto encoder = unc_encoder_factory::get_unc_encoder(image, *options);
- heif_encoding_options_free(options);
- CHECK(!encoder);
-
- // Same through the public encode entry point.
- heif_image wrapper;
- wrapper.image = image;
- CHECK(encode_uncompressed(&wrapper).code != heif_error_Ok);
- }
-}
diff --git a/tests/uncompressed_interleaved_alpha_plane_internal.cc b/tests/uncompressed_interleaved_alpha_plane_internal.cc
new file mode 100644
index 00000000..a6732dab
--- /dev/null
+++ b/tests/uncompressed_interleaved_alpha_plane_internal.cc
@@ -0,0 +1,131 @@
+/*
+ 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.
+*/
+
+// Regression test for GHSA-qfj5-c4pq-q998, internal-symbol part.
+//
+// The public-API part of this regression test lives in
+// uncompressed_interleaved_alpha_plane.cc. This file covers the defense-in-depth
+// check in the uncompressed encoder factory, which needs library-internal classes
+// (HeifPixelImage, unc_encoder_factory) and therefore only builds with full symbol
+// visibility (WITH_REDUCED_VISIBILITY=OFF).
+//
+// transfer_channel_from_image_as() moves a plane between images without going
+// through add_channel(). This is how a decoded alpha auxiliary image is attached to
+// the main image, so an interleaved image with a separate alpha plane can still be
+// assembled inside the library even though heif_image_add_plane() rejects it. The
+// uncompressed encoder must refuse such an image instead of reading past the end
+// of the interleaved component list.
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+#include "api_structs.h"
+#include "image/pixelimage.h"
+#include "codecs/uncompressed/unc_encoder.h"
+
+#include <memory>
+
+namespace {
+
+constexpr int WIDTH = 16;
+constexpr int HEIGHT = 16;
+
+struct InterleavedFormat
+{
+ heif_chroma chroma;
+ int bit_depth;
+ const char* name;
+};
+
+// The bit depths are chosen so that every interleaved encoder of the uncompressed
+// codec is covered: 8-bit RGB/RGBA use the pixel-interleave encoder, RRGGBB below
+// 14 bits the block-pixel encoder, and the remaining formats the byte-aligned one.
+const InterleavedFormat interleaved_formats[] = {
+ {heif_chroma_interleaved_RGB, 8, "RGB"},
+ {heif_chroma_interleaved_RGBA, 8, "RGBA"},
+ {heif_chroma_interleaved_RRGGBB_LE, 10, "RRGGBB_LE"},
+ {heif_chroma_interleaved_RRGGBB_BE, 16, "RRGGBB_BE"},
+ {heif_chroma_interleaved_RRGGBBAA_LE, 10, "RRGGBBAA_LE"},
+ {heif_chroma_interleaved_RRGGBBAA_BE, 16, "RRGGBBAA_BE"},
+};
+
+
+heif_error encode_uncompressed(heif_image* image)
+{
+ heif_context* ctx = heif_context_alloc();
+
+ heif_encoder* encoder = nullptr;
+ heif_error err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder);
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_encoding_options* options = heif_encoding_options_alloc();
+ options->macOS_compatibility_workaround_no_nclx_profile = true;
+
+ heif_image_handle* handle = nullptr;
+ err = heif_context_encode_image(ctx, image, encoder, options, &handle);
+
+ if (handle) {
+ heif_image_handle_release(handle);
+ }
+ heif_encoding_options_free(options);
+ heif_encoder_release(encoder);
+ heif_context_free(ctx);
+
+ return err;
+}
+
+} // namespace
+
+
+TEST_CASE("Uncompressed encoder refuses an interleaved image carrying a separate alpha plane")
+{
+ const heif_security_limits* limits = heif_get_global_security_limits();
+
+ for (const auto& fmt : interleaved_formats) {
+ INFO(fmt.name);
+
+ auto image = std::make_shared<HeifPixelImage>();
+ image->create(WIDTH, HEIGHT, heif_colorspace_RGB, fmt.chroma);
+ REQUIRE(image->fill_new_channel(heif_channel_interleaved, 0x80, WIDTH, HEIGHT, fmt.bit_depth, limits).error_code == heif_error_Ok);
+
+ auto alpha = std::make_shared<HeifPixelImage>();
+ alpha->create(WIDTH, HEIGHT, heif_colorspace_monochrome, heif_chroma_monochrome);
+ REQUIRE(alpha->fill_new_channel(heif_channel_Y, 0xFF, WIDTH, HEIGHT, fmt.bit_depth, limits).error_code == heif_error_Ok);
+
+ REQUIRE(image->transfer_channel_from_image_as(alpha, heif_channel_Y, heif_channel_Alpha).error_code == heif_error_Ok);
+ REQUIRE(image->has_channel(heif_channel_Alpha));
+
+ // The encoder factory is where the interleaved encoders are instantiated.
+ heif_encoding_options* options = heif_encoding_options_alloc();
+ auto encoder = unc_encoder_factory::get_unc_encoder(image, *options);
+ heif_encoding_options_free(options);
+ CHECK(!encoder);
+
+ // Same through the public encode entry point.
+ heif_image wrapper;
+ wrapper.image = image;
+ CHECK(encode_uncompressed(&wrapper).code != heif_error_Ok);
+ }
+}