Commit bbbbc3c3 for libheif
commit bbbbc3c33dc05da1e68b2b3357fa1d04e6fe9501
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Sat Sep 19 15:54:24 2026 +0200
Reject shrink requests in extend_to_size_with_zero (GHSA-hqc2-cx5m-g6ff)
heif_image_extend_to_size_fill_with_zero() forwarded its target width/height
unchecked to HeifPixelImage::extend_to_size_with_zero(), which only ever grows
an image. On a target smaller than the current plane size, the right-edge fill
length (subsampled_width - old_width) underflowed uint32_t and the memset wrote
past the end of the pixel plane.
Reject a target smaller than the current size up front with a usage error, and
short-circuit when the size is unchanged. Document the precondition on the C API
and add regression tests covering the shrink, grow, and unchanged cases.
The overflow is only reachable by an application calling the public API with an
out-of-contract (shrinking) size; no HEIF/AVIF file triggers it, and both
in-tree callers (extract_image_area, heif-enc tiling) only ever grow.
diff --git a/libheif/api/libheif/heif_image.h b/libheif/api/libheif/heif_image.h
index b21efed2..175ad8cc 100644
--- a/libheif/api/libheif/heif_image.h
+++ b/libheif/api/libheif/heif_image.h
@@ -298,6 +298,10 @@ heif_error heif_image_scale_image(const heif_image* input,
// Extends the image size to match the given size by extending the right and bottom borders.
// The border areas are filled with zero.
+// The target 'width' and 'height' must each be at least the image's current size; this
+// function only grows the image and cannot shrink it. If a smaller size is requested,
+// 'heif_error_Usage_error' / 'heif_suberror_Invalid_parameter_value' is returned and the
+// image is left unchanged.
LIBHEIF_API
heif_error heif_image_extend_to_size_fill_with_zero(heif_image* image,
uint32_t width, uint32_t height);
diff --git a/libheif/image/pixelimage.cc b/libheif/image/pixelimage.cc
index a7c72c27..8120071d 100644
--- a/libheif/image/pixelimage.cc
+++ b/libheif/image/pixelimage.cc
@@ -624,6 +624,22 @@ Error HeifPixelImage::extend_padding_to_size(uint32_t width, uint32_t height, bo
Error HeifPixelImage::extend_to_size_with_zero(uint32_t width, uint32_t height, const heif_security_limits* limits)
{
+ // This function only ever grows the image. A target smaller than the current
+ // size is out of contract: the per-row right-edge fill below computes its
+ // memset length as (subsampled_width - old_width), which would underflow to a
+ // huge value on a shrink request and write past the end of the pixel plane.
+ // Reject it up front (GHSA-hqc2-cx5m-g6ff).
+ if (width < m_width || height < m_height) {
+ return Error{heif_error_Usage_error,
+ heif_suberror_Invalid_parameter_value,
+ "Cannot extend an image to a size smaller than its current size."};
+ }
+
+ // Nothing to do when the target already matches the current size.
+ if (width == m_width && height == m_height) {
+ return Error::Ok;
+ }
+
for (auto& component : m_storage) {
// See extend_padding_to_size(): get_subsampled_size() assumes a non-Cb/Cr
// component has the full logical image size, so we cannot compute a correct
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 2fde1360..f993ca12 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -54,6 +54,7 @@ else()
add_libheif_test(scale_plane_checks)
add_libheif_test(crop_plane_checks)
add_libheif_test(extract_area_plane_checks)
+ add_libheif_test(extend_to_size_checks)
add_libheif_test(jpeg2000)
add_libheif_test(avc_box)
add_libheif_test(hevc_sps)
diff --git a/tests/extend_to_size_checks.cc b/tests/extend_to_size_checks.cc
new file mode 100644
index 00000000..44090dcc
--- /dev/null
+++ b/tests/extend_to_size_checks.cc
@@ -0,0 +1,115 @@
+/*
+ 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::extend_to_size_with_zero().
+//
+// This function only grows an image. The per-row right-edge fill computes its
+// memset length as (subsampled_width - old_width); on a shrink request that
+// unsigned subtraction underflows to a huge value and the memset writes far
+// past the end of the pixel plane (GHSA-hqc2-cx5m-g6ff, reachable through the
+// public heif_image_extend_to_size_fill_with_zero() API). The function now
+// rejects any target smaller than the current size before touching a plane.
+
+TEST_CASE("extend_to_size_with_zero rejects a smaller width") {
+ auto* limits = heif_get_global_security_limits();
+
+ auto image = std::make_shared<HeifPixelImage>();
+ image->create(64, 8, heif_colorspace_monochrome, heif_chroma_monochrome);
+ REQUIRE(image->add_channel(heif_channel_Y, 64, 8, 8, limits).error_code == heif_error_Ok);
+
+ // Requesting width 1 < 64 previously underflowed the fill length -> heap OOB write.
+ Error err = image->extend_to_size_with_zero(1, 8, limits);
+ REQUIRE(err.error_code == heif_error_Usage_error);
+ REQUIRE(err.sub_error_code == heif_suberror_Invalid_parameter_value);
+
+ // The image must be left unchanged on the rejected call.
+ REQUIRE(image->get_width() == 64);
+ REQUIRE(image->get_height() == 8);
+}
+
+TEST_CASE("extend_to_size_with_zero rejects a smaller height") {
+ auto* limits = heif_get_global_security_limits();
+
+ auto image = std::make_shared<HeifPixelImage>();
+ image->create(8, 64, heif_colorspace_monochrome, heif_chroma_monochrome);
+ REQUIRE(image->add_channel(heif_channel_Y, 8, 64, 8, limits).error_code == heif_error_Ok);
+
+ Error err = image->extend_to_size_with_zero(8, 1, limits);
+ REQUIRE(err.error_code == heif_error_Usage_error);
+ REQUIRE(err.sub_error_code == heif_suberror_Invalid_parameter_value);
+
+ REQUIRE(image->get_width() == 8);
+ REQUIRE(image->get_height() == 64);
+}
+
+TEST_CASE("extend_to_size_with_zero still grows an image") {
+ 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);
+
+ // Fill the visible area with a marker so we can check the padding is zero.
+ size_t stride;
+ uint8_t* p = image->get_channel_memory(heif_channel_Y, &stride);
+ for (uint32_t y = 0; y < 4; y++)
+ for (uint32_t x = 0; x < 4; x++)
+ p[y * stride + x] = 0xAB;
+
+ Error err = image->extend_to_size_with_zero(8, 8, limits);
+ REQUIRE(err.error_code == heif_error_Ok);
+ REQUIRE(image->get_width() == 8);
+ REQUIRE(image->get_height() == 8);
+
+ p = image->get_channel_memory(heif_channel_Y, &stride);
+
+ // Original pixels are preserved.
+ for (uint32_t y = 0; y < 4; y++)
+ for (uint32_t x = 0; x < 4; x++)
+ REQUIRE(p[y * stride + x] == 0xAB);
+
+ // Right and bottom borders are zero-filled.
+ for (uint32_t y = 0; y < 8; y++)
+ for (uint32_t x = 0; x < 8; x++)
+ if (x >= 4 || y >= 4)
+ REQUIRE(p[y * stride + x] == 0);
+}
+
+TEST_CASE("extend_to_size_with_zero accepts an unchanged size") {
+ 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);
+
+ Error err = image->extend_to_size_with_zero(8, 8, limits);
+ REQUIRE(err.error_code == heif_error_Ok);
+ REQUIRE(image->get_width() == 8);
+ REQUIRE(image->get_height() == 8);
+}