Commit bc7cf376 for libheif
commit bc7cf376a397b34530b867f975c261f7bb7e7c30
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon Aug 24 00:55:36 2026 +0200
scale_nearest_neighbor() rejects processing images with alpha channels of different size (GHSA-2jg2-4ch7-h545, F03)
diff --git a/libheif/image/pixelimage.cc b/libheif/image/pixelimage.cc
index ce8ac691..d131d5ad 100644
--- a/libheif/image/pixelimage.cc
+++ b/libheif/image/pixelimage.cc
@@ -804,6 +804,41 @@ bool HeifPixelImage::primary_planes_have_size(uint32_t width, uint32_t height) c
}
+bool HeifPixelImage::has_standard_plane_sizes() const
+{
+ for (const auto& component : m_storage) {
+ uint32_t expected_w, expected_h;
+
+ switch (component.m_channel) {
+ case heif_channel_Y:
+ case heif_channel_Alpha:
+ case heif_channel_R:
+ case heif_channel_G:
+ case heif_channel_B:
+ case heif_channel_interleaved:
+ expected_w = m_width;
+ expected_h = m_height;
+ break;
+
+ case heif_channel_Cb:
+ case heif_channel_Cr:
+ get_subsampled_size(m_width, m_height, component.m_channel, m_chroma, &expected_w, &expected_h);
+ break;
+
+ default:
+ // Not one of the standard channels this check knows the geometry of.
+ return false;
+ }
+
+ if (component.m_width != expected_w || component.m_height != expected_h) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
std::set<heif_channel> HeifPixelImage::get_channel_set() const
{
std::set<heif_channel> channels;
@@ -1816,10 +1851,23 @@ Error HeifPixelImage::overlay(std::shared_ptr<HeifPixelImage>& overlay, int32_t
}
+// TODO: rewrite this to handle multi-spectral images.
Error HeifPixelImage::scale_nearest_neighbor(std::shared_ptr<HeifPixelImage>& out_img,
uint32_t width, uint32_t height,
const heif_security_limits* limits) const
{
+ // The per-channel loop below indexes each source plane using coordinates
+ // derived from m_width/m_height (or, for Cb/Cr, their chroma-subsampled
+ // size), trusting that the plane actually covers that geometry. Nothing
+ // else in this class enforces that as an invariant (a plane can be added at
+ // any size through add_channel()/copy_new_channel_from()/
+ // transfer_channel_from_image_as()), so verify it explicitly before doing
+ // any work.
+ if (!has_standard_plane_sizes()) {
+ return {heif_error_Unsupported_feature, heif_suberror_Unspecified,
+ "Scaling an image with non-standard plane sizes is not supported"};
+ }
+
out_img = std::make_shared<HeifPixelImage>();
out_img->create(width, height, m_colorspace, m_chroma);
@@ -1888,6 +1936,16 @@ Error HeifPixelImage::scale_nearest_neighbor(std::shared_ptr<HeifPixelImage>& ou
}
}
+ // The per-channel loop below trusts that every source plane has a matching,
+ // correctly-sized destination plane, established by has_channel() checks
+ // per iteration.
+ // This only works for ordinary images. Images with extra planes cannot
+ // currently be scaled (TODO).
+ if (m_storage.size() > out_img->m_storage.size()) {
+ return {heif_error_Unsupported_feature, heif_suberror_Unspecified,
+ "Images with extra planes are not supported by scale_nearest_neighbor()."};
+ }
+
// --- scale all channels
diff --git a/libheif/image/pixelimage.h b/libheif/image/pixelimage.h
index 27f9dea4..3a5dc13b 100644
--- a/libheif/image/pixelimage.h
+++ b/libheif/image/pixelimage.h
@@ -107,6 +107,18 @@ public:
// undefined / custom -> not checked here; returns true
bool primary_planes_have_size(uint32_t width, uint32_t height) const;
+ // Returns true if every plane actually stored in m_storage -- not just the
+ // "primary" ones for the current colorspace -- has the size that plane's
+ // channel is expected to have: m_width x m_height for Y/Alpha/R/G/B/
+ // interleaved, and the chroma-subsampled size (via get_subsampled_size())
+ // for Cb/Cr. Any other channel type fails the check. Unlike
+ // primary_planes_have_size(), this checks each stored component directly
+ // rather than going through get_width(channel)/get_height(channel) (which
+ // only ever sees the first plane for a channel), so it also catches a
+ // same-channel duplicate whose size doesn't match, not just a missing or
+ // undersized single plane.
+ bool has_standard_plane_sizes() const;
+
heif_chroma get_chroma_format() const { return m_chroma; }
heif_colorspace get_colorspace() const { return m_colorspace; }
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index f39c97f4..66babeae 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -42,6 +42,7 @@ else()
add_libheif_test(conversion)
add_libheif_test(duplicate_alpha_channel)
add_libheif_test(idat)
+ add_libheif_test(scale_plane_checks)
add_libheif_test(jpeg2000)
add_libheif_test(avc_box)
add_libheif_test(file_layout)
diff --git a/tests/scale_plane_checks.cc b/tests/scale_plane_checks.cc
new file mode 100644
index 00000000..37bc07ce
--- /dev/null
+++ b/tests/scale_plane_checks.cc
@@ -0,0 +1,160 @@
+/*
+ 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 HeifPixelImage::scale_nearest_neighbor()'s plane checks.
+//
+// The per-channel loop that does the actual scaling indexes each source plane
+// using coordinates derived from the image's logical size, trusting that the
+// plane actually covers that geometry. Nothing about add_channel()/
+// copy_new_channel_from()/transfer_channel_from_image_as() enforces that as an
+// invariant, so two independent guards are needed:
+// - has_standard_plane_sizes() rejects any single plane (Y, Alpha, R, G, B,
+// interleaved: must match the full image size; Cb, Cr: must match the
+// chroma-subsampled size) whose actual size doesn't match what its channel
+// is expected to have -- this covers a single mismatched plane of any
+// channel, not just Alpha;
+// - a plane-count check rejects the source having more planes of a given
+// channel than were allocated for the destination (a same-channel
+// duplicate, e.g. from add_channel()/copy_new_channel_from() being called
+// twice for the same channel -- neither rejects duplicates the way
+// transfer_channel_from_image_as() does): each stored duplicate can have
+// the *correct* size and still slip past has_standard_plane_sizes(), so
+// the loop would write every one of them into the single allocated
+// destination plane, including whatever bit-depth mismatch it carries.
+
+TEST_CASE("scale_nearest_neighbor rejects a duplicate color channel") {
+ auto* limits = heif_get_global_security_limits();
+
+ auto image = std::make_shared<HeifPixelImage>();
+ image->create(4, 4, heif_colorspace_monochrome, heif_chroma_monochrome);
+ REQUIRE(image->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok);
+
+ // A second, differently-sized-per-sample Y plane. Neither add_channel() nor
+ // copy_new_channel_from() reject an already-occupied channel, so this is
+ // reachable without going through transfer_channel_from_image_as() (which
+ // does reject it).
+ REQUIRE(image->add_channel(heif_channel_Y, 4, 4, 16, limits).error_code == heif_error_Ok);
+
+ std::shared_ptr<HeifPixelImage> scaled;
+ Error err = image->scale_nearest_neighbor(scaled, 8, 8, limits);
+ REQUIRE(err.error_code == heif_error_Unsupported_feature);
+}
+
+TEST_CASE("scale_nearest_neighbor rejects an Alpha plane whose size differs from the color planes") {
+ auto* limits = heif_get_global_security_limits();
+
+ auto image = std::make_shared<HeifPixelImage>();
+ image->create(8, 8, heif_colorspace_monochrome, heif_chroma_monochrome);
+ REQUIRE(image->add_channel(heif_channel_Y, 8, 8, 8, limits).error_code == heif_error_Ok);
+ REQUIRE(image->add_channel(heif_channel_Alpha, 4, 4, 8, limits).error_code == heif_error_Ok);
+
+ std::shared_ptr<HeifPixelImage> scaled;
+ Error err = image->scale_nearest_neighbor(scaled, 16, 16, limits);
+ REQUIRE(err.error_code == heif_error_Unsupported_feature);
+}
+
+TEST_CASE("scale_nearest_neighbor rejects a single color channel whose size doesn't match the image") {
+ auto* limits = heif_get_global_security_limits();
+
+ // Not a duplicate: exactly one Y plane, but sized smaller than the image's
+ // own declared 256x256 -- reachable directly through the public plane API
+ // (heif_image_create() + heif_image_add_plane()), with no decode involved.
+ auto image = std::make_shared<HeifPixelImage>();
+ image->create(256, 256, heif_colorspace_monochrome, heif_chroma_monochrome);
+ REQUIRE(image->add_channel(heif_channel_Y, 8, 8, 8, limits).error_code == heif_error_Ok);
+
+ std::shared_ptr<HeifPixelImage> scaled;
+ Error err = image->scale_nearest_neighbor(scaled, 512, 512, limits);
+ REQUIRE(err.error_code == heif_error_Unsupported_feature);
+}
+
+TEST_CASE("scale_nearest_neighbor scales a well-formed YCbCr 4:2:0 image correctly") {
+ auto* limits = heif_get_global_security_limits();
+
+ // Cb/Cr are legitimately half-size in 4:2:0 -- has_standard_plane_sizes()
+ // must not reject that.
+ auto image = std::make_shared<HeifPixelImage>();
+ image->create(4, 4, heif_colorspace_YCbCr, heif_chroma_420);
+ REQUIRE(image->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok);
+ REQUIRE(image->add_channel(heif_channel_Cb, 2, 2, 8, limits).error_code == heif_error_Ok);
+ REQUIRE(image->add_channel(heif_channel_Cr, 2, 2, 8, limits).error_code == heif_error_Ok);
+
+ std::shared_ptr<HeifPixelImage> scaled;
+ Error err = image->scale_nearest_neighbor(scaled, 8, 8, limits);
+ REQUIRE(err.error_code == heif_error_Ok);
+ REQUIRE(scaled->get_width(heif_channel_Y) == 8);
+ REQUIRE(scaled->get_height(heif_channel_Y) == 8);
+ REQUIRE(scaled->get_width(heif_channel_Cb) == 4);
+ REQUIRE(scaled->get_height(heif_channel_Cb) == 4);
+}
+
+TEST_CASE("scale_nearest_neighbor scales a well-formed image with matching Alpha correctly") {
+ auto* limits = heif_get_global_security_limits();
+
+ auto image = std::make_shared<HeifPixelImage>();
+ image->create(2, 2, heif_colorspace_monochrome, heif_chroma_monochrome);
+ REQUIRE(image->add_channel(heif_channel_Y, 2, 2, 8, limits).error_code == heif_error_Ok);
+ REQUIRE(image->add_channel(heif_channel_Alpha, 2, 2, 8, limits).error_code == heif_error_Ok);
+
+ {
+ size_t stride;
+ uint8_t* y = image->get_channel_memory(heif_channel_Y, &stride);
+ y[0 * stride + 0] = 10;
+ y[0 * stride + 1] = 20;
+ y[1 * stride + 0] = 30;
+ y[1 * stride + 1] = 40;
+
+ uint8_t* a = image->get_channel_memory(heif_channel_Alpha, &stride);
+ a[0 * stride + 0] = 100;
+ a[0 * stride + 1] = 110;
+ a[1 * stride + 0] = 120;
+ a[1 * stride + 1] = 130;
+ }
+
+ std::shared_ptr<HeifPixelImage> scaled;
+ Error err = image->scale_nearest_neighbor(scaled, 4, 4, limits);
+ REQUIRE(err.error_code == heif_error_Ok);
+ REQUIRE(scaled->get_width(heif_channel_Y) == 4);
+ REQUIRE(scaled->get_height(heif_channel_Y) == 4);
+ REQUIRE(scaled->has_channel(heif_channel_Alpha));
+ REQUIRE(scaled->get_width(heif_channel_Alpha) == 4);
+ REQUIRE(scaled->get_height(heif_channel_Alpha) == 4);
+
+ // Nearest-neighbor 2x2 -> 4x4: each source pixel covers a 2x2 block.
+ size_t stride;
+ const uint8_t* y = scaled->get_channel_memory(heif_channel_Y, &stride);
+ REQUIRE(y[0 * stride + 0] == 10);
+ REQUIRE(y[0 * stride + 3] == 20);
+ REQUIRE(y[3 * stride + 0] == 30);
+ REQUIRE(y[3 * stride + 3] == 40);
+
+ const uint8_t* a = scaled->get_channel_memory(heif_channel_Alpha, &stride);
+ REQUIRE(a[0 * stride + 0] == 100);
+ REQUIRE(a[3 * stride + 3] == 130);
+}