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