Commit f4fb8bde for libheif
commit f4fb8bde4704ebb46e46ff9fb94407c9774153b2
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Sun Aug 23 21:22:04 2026 +0200
Reject duplicate/mismatched-size Alpha planes (GHSA-g89c-p67h-r497)
diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index 567f3825..db31203e 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -1073,7 +1073,9 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem::decode_image(const heif_decod
}
alpha = std::move(scaled_alpha);
}
- img->transfer_channel_from_image_as(alpha, channel, heif_channel_Alpha);
+ if (Error err = img->transfer_channel_from_image_as(alpha, channel, heif_channel_Alpha)) {
+ return err;
+ }
if (is_premultiplied_alpha()) {
img->set_premultiplied_alpha(true);
diff --git a/libheif/image/pixelimage.cc b/libheif/image/pixelimage.cc
index b2201ff7..a43601c7 100644
--- a/libheif/image/pixelimage.cc
+++ b/libheif/image/pixelimage.cc
@@ -1030,11 +1030,21 @@ void HeifPixelImage::fill_channel(heif_channel dst_channel, uint16_t value)
}
-void HeifPixelImage::transfer_channel_from_image_as(const std::shared_ptr<HeifPixelImage>& source,
+Error HeifPixelImage::transfer_channel_from_image_as(const std::shared_ptr<HeifPixelImage>& source,
heif_channel src_channel,
heif_channel dst_channel)
{
- // TODO: check that dst_channel does not exist yet
+ // A destination image must never end up with two planes for the same channel:
+ // find_storage_for_channel() and every method built on it (get_bits_per_pixel(),
+ // get_channel_memory(), get_width()/get_height()) only ever look at the first
+ // match, so a second, differently-sized/differently-typed plane for the same
+ // channel would silently be invisible to size queries while still being iterated
+ // (and written to) by code that walks m_storage directly, e.g. scale_nearest_neighbor().
+ if (find_storage_for_channel(dst_channel) != nullptr) {
+ return {heif_error_Invalid_input,
+ heif_suberror_Unspecified,
+ "Destination image already has a plane for this channel"};
+ }
// Find and remove the component from source
ComponentStorage plane;
@@ -1085,6 +1095,8 @@ void HeifPixelImage::transfer_channel_from_image_as(const std::shared_ptr<HeifPi
m_memory_handle.alloc(plane.allocation_size,
source->m_memory_handle.get_security_limits(),
"transferred image data");
+
+ return Error::Ok;
}
@@ -1657,6 +1669,21 @@ Error HeifPixelImage::overlay(std::shared_ptr<HeifPixelImage>& overlay, int32_t
bool has_alpha = overlay->has_channel(heif_channel_Alpha);
//bool has_alpha_me = has_channel(heif_channel_Alpha);
+ // The blend loop below indexes the Alpha plane using the extent of each color
+ // channel (in_w/in_h, out_w/out_h), not the Alpha plane's own reported extent.
+ // If the Alpha plane were smaller than the other channels, that would read past
+ // its allocation, so reject that case up front instead of trusting the sizes
+ // to agree.
+ // Note that differently sized Alpha channels are allowed, but we currently do
+ // not support it here (TODO).
+ if (has_alpha &&
+ (overlay->get_width(heif_channel_Alpha) != overlay->get_width() ||
+ overlay->get_height(heif_channel_Alpha) != overlay->get_height())) {
+ return {heif_error_Unsupported_feature,
+ heif_suberror_Unspecified,
+ "Overlay image Alpha plane size does not match the other color planes"};
+ }
+
size_t alpha_stride = 0;
uint8_t* alpha_p;
alpha_p = overlay->get_channel_memory(heif_channel_Alpha, &alpha_stride);
diff --git a/libheif/image/pixelimage.h b/libheif/image/pixelimage.h
index e33d9f66..27f9dea4 100644
--- a/libheif/image/pixelimage.h
+++ b/libheif/image/pixelimage.h
@@ -268,7 +268,7 @@ public:
Error fill_new_channel(heif_channel dst_channel, uint16_t value, int width, int height, int bpp, const heif_security_limits* limits);
- void transfer_channel_from_image_as(const std::shared_ptr<HeifPixelImage>& source,
+ Error transfer_channel_from_image_as(const std::shared_ptr<HeifPixelImage>& source,
heif_channel src_channel,
heif_channel dst_channel);
diff --git a/libheif/sequences/track_visual.cc b/libheif/sequences/track_visual.cc
index f72ee038..60c98c1a 100644
--- a/libheif/sequences/track_visual.cc
+++ b/libheif/sequences/track_visual.cc
@@ -316,7 +316,9 @@ Result<std::shared_ptr<HeifPixelImage> > Track_Visual::decode_next_image_sample(
alphaImage = std::move(scaled_alpha);
}
- image->transfer_channel_from_image_as(alphaImage, heif_channel_Y, heif_channel_Alpha);
+ if (Error err = image->transfer_channel_from_image_as(alphaImage, heif_channel_Y, heif_channel_Alpha)) {
+ return err;
+ }
}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 8a572d84..c0d890a6 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -40,6 +40,7 @@ else()
add_libheif_test(box_equals)
add_libheif_test(clap_zero_size)
add_libheif_test(conversion)
+ add_libheif_test(duplicate_alpha_channel)
add_libheif_test(idat)
add_libheif_test(jpeg2000)
add_libheif_test(avc_box)
diff --git a/tests/duplicate_alpha_channel.cc b/tests/duplicate_alpha_channel.cc
new file mode 100644
index 00000000..eddc98f2
--- /dev/null
+++ b/tests/duplicate_alpha_channel.cc
@@ -0,0 +1,109 @@
+/*
+ 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.
+*/
+
+#include "image/pixelimage.h"
+#include "catch_amalgamated.hpp"
+
+// Regression tests for GHSA-g89c-p67h-r497.
+
+TEST_CASE("transfer_channel_from_image_as rejects a duplicate destination channel")
+{
+ auto* limits = heif_get_global_security_limits();
+
+ auto dst = std::make_shared<HeifPixelImage>();
+ dst->create(4, 4, heif_colorspace_monochrome, heif_chroma_monochrome);
+ REQUIRE(dst->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok);
+
+ // Attaching a channel the destination doesn't have yet succeeds.
+ auto alpha1 = std::make_shared<HeifPixelImage>();
+ alpha1->create(4, 4, heif_colorspace_monochrome, heif_chroma_monochrome);
+ REQUIRE(alpha1->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok);
+
+ Error err1 = dst->transfer_channel_from_image_as(alpha1, heif_channel_Y, heif_channel_Alpha);
+ REQUIRE(err1.error_code == heif_error_Ok);
+ REQUIRE(dst->has_channel(heif_channel_Alpha));
+ REQUIRE(dst->get_bits_per_pixel(heif_channel_Alpha) == 8);
+
+ // A second attach to the same, already-occupied destination channel must be
+ // rejected instead of silently appending a duplicate plane.
+ auto alpha2 = std::make_shared<HeifPixelImage>();
+ alpha2->create(4, 4, heif_colorspace_monochrome, heif_chroma_monochrome);
+ REQUIRE(alpha2->add_channel(heif_channel_Y, 4, 4, 10, limits).error_code == heif_error_Ok);
+
+ Error err2 = dst->transfer_channel_from_image_as(alpha2, heif_channel_Y, heif_channel_Alpha);
+ REQUIRE(err2.error_code == heif_error_Invalid_input);
+
+ // The destination must still describe only the original 8-bit Alpha plane.
+ REQUIRE(dst->get_bits_per_pixel(heif_channel_Alpha) == 8);
+
+ // The rejected source must be left untouched: the duplicate check must run
+ // before any plane is moved out of it, not after.
+ REQUIRE(alpha2->has_channel(heif_channel_Y));
+}
+
+TEST_CASE("overlay rejects an Alpha plane whose size does not match the other channels")
+{
+ auto* limits = heif_get_global_security_limits();
+
+ auto base = std::make_shared<HeifPixelImage>();
+ base->create(4, 4, heif_colorspace_monochrome, heif_chroma_monochrome);
+ REQUIRE(base->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok);
+
+ // An overlay image whose Alpha plane is smaller than its own color plane
+ // must be rejected up front, since the blend loop indexes the Alpha plane
+ // using the color channel's extent.
+ auto overlay_img = std::make_shared<HeifPixelImage>();
+ overlay_img->create(4, 4, heif_colorspace_monochrome, heif_chroma_monochrome);
+ REQUIRE(overlay_img->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok);
+ REQUIRE(overlay_img->add_channel(heif_channel_Alpha, 2, 2, 8, limits).error_code == heif_error_Ok);
+
+ Error err = base->overlay(overlay_img, 0, 0);
+ REQUIRE(err.error_code == heif_error_Invalid_input);
+}
+
+TEST_CASE("overlay blends normally when the Alpha plane size matches")
+{
+ auto* limits = heif_get_global_security_limits();
+
+ auto base = std::make_shared<HeifPixelImage>();
+ base->create(2, 2, heif_colorspace_monochrome, heif_chroma_monochrome);
+ REQUIRE(base->add_channel(heif_channel_Y, 2, 2, 8, limits).error_code == heif_error_Ok);
+ base->fill_channel(heif_channel_Y, 0);
+
+ auto overlay_img = std::make_shared<HeifPixelImage>();
+ overlay_img->create(2, 2, heif_colorspace_monochrome, heif_chroma_monochrome);
+ REQUIRE(overlay_img->add_channel(heif_channel_Y, 2, 2, 8, limits).error_code == heif_error_Ok);
+ REQUIRE(overlay_img->add_channel(heif_channel_Alpha, 2, 2, 8, limits).error_code == heif_error_Ok);
+ overlay_img->fill_channel(heif_channel_Y, 200);
+ overlay_img->fill_channel(heif_channel_Alpha, 255); // fully opaque
+
+ Error err = base->overlay(overlay_img, 0, 0);
+ REQUIRE(err.error_code == heif_error_Ok);
+
+ size_t stride;
+ const uint8_t* data = base->get_channel_memory(heif_channel_Y, &stride);
+ REQUIRE(data[0] == 200);
+}