Commit c0a63aaa for libheif
commit c0a63aaa29f511aacb462863aa69b274dbc3aef7
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon Aug 24 01:01:12 2026 +0200
crop() rejects processing images with alpha channels of different size (GHSA-2jg2-4ch7-h545, F04)
diff --git a/libheif/image/pixelimage.cc b/libheif/image/pixelimage.cc
index d131d5ad..6f1324de 100644
--- a/libheif/image/pixelimage.cc
+++ b/libheif/image/pixelimage.cc
@@ -1507,6 +1507,18 @@ Result<std::shared_ptr<HeifPixelImage>> HeifPixelImage::crop(uint32_t left, uint
"Invalid crop region"};
}
+ // ComponentStorage::crop() below maps (left,right,top,bottom) -- already bounded
+ // against m_width/m_height above -- into per-plane coordinates via
+ // get_subsampled_size_h/v(), then bulk-memcpy's each row assuming the plane
+ // actually covers that geometry. Verify that first; a plane smaller than
+ // m_width/m_height implies (e.g. from Alpha attached without a size check, or
+ // any other producer that doesn't enforce this) would otherwise be read past
+ // its end.
+ if (!has_standard_plane_sizes()) {
+ return Error{heif_error_Unsupported_feature, heif_suberror_Unspecified,
+ "Cropping an image with non-standard plane sizes is not supported"};
+ }
+
// --- for some subsampled chroma colorspaces, we have to transform to 4:4:4 before cropping
bool need_conversion = false;
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 66babeae..74e093b3 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -43,6 +43,7 @@ else()
add_libheif_test(duplicate_alpha_channel)
add_libheif_test(idat)
add_libheif_test(scale_plane_checks)
+ add_libheif_test(crop_plane_checks)
add_libheif_test(jpeg2000)
add_libheif_test(avc_box)
add_libheif_test(file_layout)
diff --git a/tests/crop_plane_checks.cc b/tests/crop_plane_checks.cc
new file mode 100644
index 00000000..536e214e
--- /dev/null
+++ b/tests/crop_plane_checks.cc
@@ -0,0 +1,107 @@
+/*
+ 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::crop().
+//
+// crop() validates the requested rectangle against the image's declared
+// m_width/m_height, maps it into per-plane coordinates via
+// get_subsampled_size_h/v(), and hands it to ComponentStorage::crop(), which
+// does a row-wise bulk memcpy. None of that checks that a plane's actual
+// stored size matches what m_width/m_height (or, for Cb/Cr, their
+// chroma-subsampled size) implies -- a plane can be added at any size through
+// add_channel()/copy_new_channel_from()/transfer_channel_from_image_as(). A
+// plane smaller than the image claims is read past its end. crop() now calls
+// has_standard_plane_sizes() up front to reject that instead.
+
+TEST_CASE("crop rejects a plane smaller than the image's declared size") {
+ auto* limits = heif_get_global_security_limits();
+
+ auto image = std::make_shared<HeifPixelImage>();
+ image->create(8, 8, heif_colorspace_monochrome, heif_chroma_monochrome);
+
+ // Y plane smaller than the image's declared 8x8 -- add_channel() doesn't
+ // check this against m_width/m_height.
+ REQUIRE(image->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok);
+
+ auto result = image->crop(0, 3, 0, 3, limits);
+ REQUIRE(result.is_error());
+ REQUIRE(result.error().error_code == heif_error_Unsupported_feature);
+}
+
+TEST_CASE("crop crops a well-formed monochrome image correctly") {
+ 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);
+
+ size_t stride;
+ uint8_t* y = image->get_channel_memory(heif_channel_Y, &stride);
+ for (uint32_t row = 0; row < 4; row++) {
+ for (uint32_t col = 0; col < 4; col++) {
+ y[row * stride + col] = static_cast<uint8_t>(row * 4 + col);
+ }
+ }
+
+ // Keep the inner 2x2 region: columns/rows 1..2.
+ auto result = image->crop(1, 2, 1, 2, limits);
+ REQUIRE(!result.is_error());
+
+ auto cropped = *result;
+ REQUIRE(cropped->get_width(heif_channel_Y) == 2);
+ REQUIRE(cropped->get_height(heif_channel_Y) == 2);
+
+ const uint8_t* out = cropped->get_channel_memory(heif_channel_Y, &stride);
+ REQUIRE(out[0 * stride + 0] == 5); // (1,1)
+ REQUIRE(out[0 * stride + 1] == 6); // (2,1)
+ REQUIRE(out[1 * stride + 0] == 9); // (1,2)
+ REQUIRE(out[1 * stride + 1] == 10); // (2,2)
+}
+
+TEST_CASE("crop crops 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);
+
+ // Crop to an even-aligned 2x2 region so no 4:4:4 upconversion is triggered.
+ auto result = image->crop(0, 1, 0, 1, limits);
+ REQUIRE(!result.is_error());
+
+ auto cropped = *result;
+ REQUIRE(cropped->get_width(heif_channel_Y) == 2);
+ REQUIRE(cropped->get_height(heif_channel_Y) == 2);
+ REQUIRE(cropped->get_width(heif_channel_Cb) == 1);
+ REQUIRE(cropped->get_height(heif_channel_Cb) == 1);
+}