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