Commit f01870c1 for libheif
commit f01870c1d7323a3003796d58eba7fff502be994c
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Thu Jun 25 19:58:57 2026 +0200
Fix clap transform double-application in image tiling (GHSA-jc8f-p23p-5hjg)
The base ImageItem::get_heif_image_tiling() returned the already
transformed m_width/m_height, but process_image_transformations_on_tiling()
applies the transformative properties (irot, imir, clap) itself. This
applied every transform twice. For a clap that rounds the image down to
zero, the second application passed 0 into Box_clap::left_rounded(), where
`image_width - 1U` underflowed to UINT32_MAX and overflowed the Fraction
constructor (assert abort in debug builds, corrupt crop in release builds).
The grid, unc and tiled overrides already return coded dimensions, so the
base class was the lone outlier.
Fixes, in three layers:
- image_item.cc: base get_heif_image_tiling() now reports coded (ispe)
dimensions when available, matching the other overrides, so transforms
are applied exactly once. This also fixes a silent irot/imir
double-transform on the same path.
- context.cc: reject a clap that rounds a dimension to zero or less at
parse time, mirroring the existing ispe zero-size check.
- box.cc: guard left_rounded()/top_rounded() against a zero image
dimension as defense in depth.
Add tests/clap_zero_size.cc covering the hardened clap helpers.
diff --git a/libheif/box.cc b/libheif/box.cc
index 984967f7..68e956e6 100644
--- a/libheif/box.cc
+++ b/libheif/box.cc
@@ -3729,6 +3729,12 @@ int Box_clap::left_rounded(uint32_t image_width) const
// left = horizOff + (width-1)/2 - (clapWidth-1)/2
+ // Guard against image_width==0: `image_width - 1U` would underflow to
+ // UINT32_MAX and overflow the Fraction (GHSA-jc8f-p23p-5hjg).
+ if (image_width == 0) {
+ return 0;
+ }
+
Fraction pcX = m_horizontal_offset + Fraction(image_width - 1U, 2U);
Fraction left = pcX - (m_clean_aperture_width - 1) / 2;
@@ -3744,6 +3750,11 @@ int Box_clap::right_rounded(uint32_t image_width) const
int Box_clap::top_rounded(uint32_t image_height) const
{
+ // Guard against image_height==0 underflowing the Fraction (see left_rounded).
+ if (image_height == 0) {
+ return 0;
+ }
+
Fraction pcY = m_vertical_offset + Fraction(image_height - 1U, 2U);
Fraction top = pcY - (m_clean_aperture_height - 1) / 2;
diff --git a/libheif/context.cc b/libheif/context.cc
index eb22f642..6c8f2931 100644
--- a/libheif/context.cc
+++ b/libheif/context.cc
@@ -767,8 +767,16 @@ Error HeifContext::interpret_heif_file_images()
for (const auto& prop : properties) {
auto clap = std::dynamic_pointer_cast<Box_clap>(prop);
if (clap) {
- image->set_resolution(clap->get_width_rounded(),
- clap->get_height_rounded());
+ int clap_width = clap->get_width_rounded();
+ int clap_height = clap->get_height_rounded();
+ if (clap_width <= 0 || clap_height <= 0) {
+ return {heif_error_Invalid_input,
+ heif_suberror_Invalid_clean_aperture,
+ "Clean aperture (clap) reduces image to zero size"};
+ }
+
+ image->set_resolution(static_cast<uint32_t>(clap_width),
+ static_cast<uint32_t>(clap_height));
if (image->has_intrinsic_matrix()) {
image->get_intrinsic_matrix().apply_clap(clap.get(), image->get_width(), image->get_height());
diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index 7495d766..567f3825 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -1319,10 +1319,25 @@ heif_image_tiling ImageItem::get_heif_image_tiling() const
tiling.num_columns = 1;
tiling.num_rows = 1;
- tiling.tile_width = m_width;
- tiling.tile_height = m_height;
- tiling.image_width = m_width;
- tiling.image_height = m_height;
+ // Report the coded (pre-transformation) dimensions here. The caller applies
+ // the transformative properties (irot, imir, clap) via
+ // process_image_transformations_on_tiling(), so handing it the already
+ // transformed m_width/m_height would apply them a second time. For a clap
+ // that shrinks the image to zero this double application underflowed inside
+ // Box_clap::left_rounded() (GHSA-jc8f-p23p-5hjg); for irot/imir it silently
+ // produced wrong dimensions. The grid/unc/tiled overrides likewise report
+ // coded dimensions.
+ uint32_t coded_width = m_width;
+ uint32_t coded_height = m_height;
+ if (has_ispe_resolution()) {
+ coded_width = get_ispe_width();
+ coded_height = get_ispe_height();
+ }
+
+ tiling.tile_width = coded_width;
+ tiling.tile_height = coded_height;
+ tiling.image_width = coded_width;
+ tiling.image_height = coded_height;
tiling.top_offset = 0;
tiling.left_offset = 0;
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 940acfd9..cba24f24 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -38,6 +38,7 @@ if (WITH_REDUCED_VISIBILITY)
else()
add_libheif_test(bitstream_tests)
add_libheif_test(box_equals)
+ add_libheif_test(clap_zero_size)
add_libheif_test(conversion)
add_libheif_test(idat)
add_libheif_test(jpeg2000)
diff --git a/tests/clap_zero_size.cc b/tests/clap_zero_size.cc
new file mode 100644
index 00000000..eafc1258
--- /dev/null
+++ b/tests/clap_zero_size.cc
@@ -0,0 +1,42 @@
+/*
+ libheif clean aperture (clap) zero-size 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 "catch_amalgamated.hpp"
+#include "box.h"
+
+// Regression test for GHSA-jc8f-p23p-5hjg: passing a zero image dimension to
+// the clap rounding helpers used to underflow `image_width - 1U` to UINT32_MAX,
+// which overflowed the Fraction constructor (assert abort in debug builds,
+// corrupt crop in release builds). They must now return 0 without aborting.
+TEST_CASE("clap rounding with zero image size") {
+ std::shared_ptr<Box_clap> clap = std::make_shared<Box_clap>();
+ clap->set(100, 200, 150, 250); // clap 100x200 inside a 150x250 image
+
+ REQUIRE(clap->left_rounded(0) == 0);
+ REQUIRE(clap->right_rounded(0) == 99); // clapWidth - 1 + left(0)
+ REQUIRE(clap->top_rounded(0) == 0);
+ REQUIRE(clap->bottom_rounded(0) == 199); // clapHeight - 1 + top(0)
+}