Commit 1c2df9c0 for libheif
commit 1c2df9c0e814f89716f3a4d85a444b2dbc63ad69
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Tue Aug 25 22:31:15 2026 +0200
clap: return an error instead of asserting for image sizes outside the Fraction range (GHSA-gh5q-69gg-c964)
An 'ispe' dimension above INT32_MAX + 1 combined with a 'clap' property reached
the Fraction(uint32_t, uint32_t) constructor, which only assert()ed its range.
This aborted assert-enabled builds through heif_image_handle_get_image_tiling()
(either clap helper, depending on 'irot') and computed a bogus crop in release
builds. The GHSA-jc8f-p23p-5hjg fix had only guarded the zero-size underflow.
Fix this at the root instead of guarding call sites:
- Replace the asserting constructor with the checked factories
Fraction::from_signed() / from_unsigned() returning Result<Fraction>.
- Replace the four Box_clap::*_rounded() helpers with Box_clap::get_crop(),
which validates the image size once and returns a Result<Crop>.
- Box_clap::set() and RegionCoordinateTransform::create() report errors.
- Propagate the error through the decode path, the tiling API, the region
coordinate transforms and heif_item_get_property_transform_crop_borders()
(which keeps its void signature and reports zero borders).
- heif_image_handle_get_image_tiling() applies the security limits to the tile
size, so plain items are rejected consistently with the decoding path while
tiled images larger than the limit remain usable tile by tile.
No public C signature changes. Add regression tests, including the reporter's
four reproducer files.
diff --git a/libheif/api/libheif/heif_properties.cc b/libheif/api/libheif/heif_properties.cc
index 84ca65ae..1d58edf3 100644
--- a/libheif/api/libheif/heif_properties.cc
+++ b/libheif/api/libheif/heif_properties.cc
@@ -245,10 +245,22 @@ void heif_item_get_property_transform_crop_borders(const heif_context* context,
return;
}
- if (left) *left = (*clap)->left_rounded(image_width);
- if (right) *right = image_width - 1 - (*clap)->right_rounded(image_width);
- if (top) *top = (*clap)->top_rounded(image_height);
- if (bottom) *bottom = image_height - 1 - (*clap)->bottom_rounded(image_height);
+ auto crop = (*clap)->get_crop(image_width, image_height);
+ if (!crop) {
+ // The clean aperture cannot be applied to an image of this size (zero size, negative
+ // size, or a size beyond the supported coordinate range). This function has no error
+ // return, so report that nothing is cropped.
+ if (left) *left = 0;
+ if (right) *right = 0;
+ if (top) *top = 0;
+ if (bottom) *bottom = 0;
+ return;
+ }
+
+ if (left) *left = crop->left;
+ if (right) *right = image_width - 1 - crop->right;
+ if (top) *top = crop->top;
+ if (bottom) *bottom = image_height - 1 - crop->bottom;
}
diff --git a/libheif/api/libheif/heif_properties.h b/libheif/api/libheif/heif_properties.h
index d1fc6528..40a6a91d 100644
--- a/libheif/api/libheif/heif_properties.h
+++ b/libheif/api/libheif/heif_properties.h
@@ -131,6 +131,8 @@ int heif_item_get_property_transform_rotation_ccw(const heif_context* context,
// Because of the way this data is stored, you have to pass the image size at the moment of the crop operation
// to compute the cropped border sizes.
// If 'propertyId==0', it returns the first clap property found.
+// If the clap cannot be applied to the given image size (zero size or a size beyond the
+// supported coordinate range), all four borders are set to 0.
LIBHEIF_API
void heif_item_get_property_transform_crop_borders(const heif_context* context,
heif_item_id itemId,
diff --git a/libheif/api/libheif/heif_regions.cc b/libheif/api/libheif/heif_regions.cc
index c77dd993..ee550504 100644
--- a/libheif/api/libheif/heif_regions.cc
+++ b/libheif/api/libheif/heif_regions.cc
@@ -173,7 +173,10 @@ heif_error heif_region_get_point_transformed(const struct heif_region* region, h
auto t = RegionCoordinateTransform::create(region->context->get_heif_file(), image_id,
region->region_item->reference_width,
region->region_item->reference_height);
- RegionCoordinateTransform::Point p = t.transform_point({(double) point->x, (double) point->y});
+ if (!t) {
+ return t.error().error_struct(region->context.get());
+ }
+ RegionCoordinateTransform::Point p = t->transform_point({(double) point->x, (double) point->y});
*x = p.x;
*y = p.y;
@@ -211,9 +214,12 @@ heif_error heif_region_get_rectangle_transformed(const heif_region* region,
auto t = RegionCoordinateTransform::create(region->context->get_heif_file(), image_id,
region->region_item->reference_width,
region->region_item->reference_height);
+ if (!t) {
+ return t.error().error_struct(region->context.get());
+ }
- RegionCoordinateTransform::Point p = t.transform_point({(double) rect->x, (double) rect->y});
- RegionCoordinateTransform::Extent e = t.transform_extent({(double) rect->width, (double) rect->height});
+ RegionCoordinateTransform::Point p = t->transform_point({(double) rect->x, (double) rect->y});
+ RegionCoordinateTransform::Extent e = t->transform_extent({(double) rect->width, (double) rect->height});
*x = p.x;
*y = p.y;
@@ -253,9 +259,12 @@ heif_error heif_region_get_ellipse_transformed(const heif_region* region,
auto t = RegionCoordinateTransform::create(region->context->get_heif_file(), image_id,
region->region_item->reference_width,
region->region_item->reference_height);
+ if (!t) {
+ return t.error().error_struct(region->context.get());
+ }
- RegionCoordinateTransform::Point p = t.transform_point({(double) ellipse->x, (double) ellipse->y});
- RegionCoordinateTransform::Extent e = t.transform_extent({(double) ellipse->radius_x, (double) ellipse->radius_y});
+ RegionCoordinateTransform::Point p = t->transform_point({(double) ellipse->x, (double) ellipse->y});
+ RegionCoordinateTransform::Extent e = t->transform_extent({(double) ellipse->radius_x, (double) ellipse->radius_y});
*x = p.x;
*y = p.y;
@@ -319,9 +328,12 @@ static heif_error heif_region_get_poly_points_scaled(const heif_region* region,
auto t = RegionCoordinateTransform::create(region->context->get_heif_file(), image_id,
region->region_item->reference_width,
region->region_item->reference_height);
+ if (!t) {
+ return t.error().error_struct(region->context.get());
+ }
for (int i = 0; i < (int) poly->points.size(); i++) {
- RegionCoordinateTransform::Point p = t.transform_point({
+ RegionCoordinateTransform::Point p = t->transform_point({
(double) poly->points[i].x,
(double) poly->points[i].y
});
diff --git a/libheif/api/libheif/heif_tiling.cc b/libheif/api/libheif/heif_tiling.cc
index 66e41559..2cc3cdaf 100644
--- a/libheif/api/libheif/heif_tiling.cc
+++ b/libheif/api/libheif/heif_tiling.cc
@@ -22,6 +22,7 @@
#include "api_structs.h"
#include "image-items/grid.h"
#include "image-items/tiled.h"
+#include "security_limits.h"
#if WITH_UNCOMPRESSED_CODEC
#include "image-items/unc_image.h"
@@ -41,6 +42,21 @@ heif_error heif_image_handle_get_image_tiling(const heif_image_handle* handle, i
*tiling = handle->image->get_heif_image_tiling();
+ // Every tile has to be decodable on its own, so apply the same size limit that the
+ // decoding path applies to the 'ispe' size. For plain (non-tiled) items, the single
+ // tile is the whole image. The whole-image size is deliberately not checked here,
+ // because tiled images larger than the limit can still be decoded tile by tile.
+ // A tile size of zero means that the size is unknown (e.g. a grid whose first tile
+ // is missing); there is nothing to check in that case.
+ // (GHSA-gh5q-69gg-c964: the tiling API returned dimensions that no other part of
+ // the library accepts.)
+ if (tiling->tile_width != 0 && tiling->tile_height != 0) {
+ if (Error err = check_for_valid_image_size(handle->context->get_security_limits(),
+ tiling->tile_width, tiling->tile_height)) {
+ return err.error_struct(handle->context.get());
+ }
+ }
+
if (process_image_transformations) {
Error error = handle->image->process_image_transformations_on_tiling(*tiling);
if (error) {
diff --git a/libheif/box.cc b/libheif/box.cc
index 74ea8718..4f571e13 100644
--- a/libheif/box.cc
+++ b/libheif/box.cc
@@ -80,12 +80,28 @@ Fraction::Fraction(int32_t num, int32_t den)
}
}
-Fraction::Fraction(uint32_t num, uint32_t den)
+Result<Fraction> Fraction::from_signed(int64_t num, int64_t den)
{
- assert(num <= (uint32_t) std::numeric_limits<int32_t>::max());
- assert(den <= (uint32_t) std::numeric_limits<int32_t>::max());
+ if (num < std::numeric_limits<int32_t>::min() || num > std::numeric_limits<int32_t>::max() ||
+ den < std::numeric_limits<int32_t>::min() || den > std::numeric_limits<int32_t>::max()) {
+ return Error(heif_error_Invalid_input,
+ heif_suberror_Invalid_fractional_number,
+ "Fraction value exceeds the supported range");
+ }
+
+ Fraction f(static_cast<int32_t>(num), static_cast<int32_t>(den));
+ if (!f.is_valid()) {
+ return Error(heif_error_Invalid_input,
+ heif_suberror_Invalid_fractional_number,
+ "Fraction with zero denominator");
+ }
- *this = Fraction(int32_t(num), int32_t(den));
+ return f;
+}
+
+Result<Fraction> Fraction::from_unsigned(uint32_t num, uint32_t den)
+{
+ return from_signed(num, den);
}
Fraction::Fraction(int64_t num, int64_t den)
@@ -3646,28 +3662,24 @@ Error Box_clap::parse(BitstreamRange& range, const heif_security_limits* limits)
int32_t vertical_offset_num = (int32_t) range.read32();
uint32_t vertical_offset_den = (uint32_t) range.read32();
- if (clean_aperture_width_num > (uint32_t) std::numeric_limits<int32_t>::max() ||
- clean_aperture_width_den > (uint32_t) std::numeric_limits<int32_t>::max() ||
- clean_aperture_height_num > (uint32_t) std::numeric_limits<int32_t>::max() ||
- clean_aperture_height_den > (uint32_t) std::numeric_limits<int32_t>::max() ||
- horizontal_offset_den > (uint32_t) std::numeric_limits<int32_t>::max() ||
- vertical_offset_den > (uint32_t) std::numeric_limits<int32_t>::max()) {
- return Error(heif_error_Invalid_input,
- heif_suberror_Invalid_fractional_number,
- "Exceeded supported value range.");
+ // The checked Fraction construction rejects values outside the int32_t range and
+ // zero denominators.
+ auto clean_aperture_width = Fraction::from_unsigned(clean_aperture_width_num, clean_aperture_width_den);
+ auto clean_aperture_height = Fraction::from_unsigned(clean_aperture_height_num, clean_aperture_height_den);
+ auto horizontal_offset = Fraction::from_signed(horizontal_offset_num, horizontal_offset_den);
+ auto vertical_offset = Fraction::from_signed(vertical_offset_num, vertical_offset_den);
+
+ for (const Result<Fraction>* f : {&clean_aperture_width, &clean_aperture_height,
+ &horizontal_offset, &vertical_offset}) {
+ if (!*f) {
+ return f->error();
+ }
}
- m_clean_aperture_width = Fraction(clean_aperture_width_num,
- clean_aperture_width_den);
- m_clean_aperture_height = Fraction(clean_aperture_height_num,
- clean_aperture_height_den);
- m_horizontal_offset = Fraction(horizontal_offset_num, (int32_t) horizontal_offset_den);
- m_vertical_offset = Fraction(vertical_offset_num, (int32_t) vertical_offset_den);
- if (!m_clean_aperture_width.is_valid() || !m_clean_aperture_height.is_valid() ||
- !m_horizontal_offset.is_valid() || !m_vertical_offset.is_valid()) {
- return Error(heif_error_Invalid_input,
- heif_suberror_Invalid_fractional_number);
- }
+ m_clean_aperture_width = *clean_aperture_width;
+ m_clean_aperture_height = *clean_aperture_height;
+ m_horizontal_offset = *horizontal_offset;
+ m_vertical_offset = *vertical_offset;
return range.get_error();
}
@@ -3725,50 +3737,45 @@ double Box_clap::top(int image_height) const
}
-int Box_clap::left_rounded(uint32_t image_width) const
+Result<Box_clap::Crop> Box_clap::get_crop(uint32_t image_width, uint32_t image_height) const
{
- // pcX = horizOff + (width - 1)/2
- // pcX ± (cleanApertureWidth - 1)/2
-
- // 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;
+ // The clean aperture is specified relative to the image center:
+ // pcX = horizOff + (width - 1)/2
+ // left = pcX - (clapWidth - 1)/2
+ // right = left + clapWidth - 1
+ // (and likewise vertically).
+ //
+ // Computing this needs the image size inside the int32_t-based Fraction. A zero size
+ // would underflow `size - 1` (GHSA-jc8f-p23p-5hjg) and a size above INT32_MAX + 1
+ // does not fit (GHSA-gh5q-69gg-c964). Such an image cannot be cropped with a 'clap',
+ // which is reported as an error instead of computing a bogus crop.
+ if (image_width == 0 || image_height == 0) {
+ return Error(heif_error_Invalid_input,
+ heif_suberror_Invalid_clean_aperture,
+ "Clean aperture cannot be applied to an image with zero size");
}
- Fraction pcX = m_horizontal_offset + Fraction(image_width - 1U, 2U);
- Fraction left = pcX - (m_clean_aperture_width - 1) / 2;
-
- return left.round_down();
-}
-
-int Box_clap::right_rounded(uint32_t image_width) const
-{
- Fraction right = m_clean_aperture_width - 1 + left_rounded(image_width);
-
- return right.round();
-}
-
-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;
+ auto halfWidth = Fraction::from_unsigned(image_width - 1, 2);
+ auto halfHeight = Fraction::from_unsigned(image_height - 1, 2);
+ if (!halfWidth || !halfHeight) {
+ return Error(heif_error_Invalid_input,
+ heif_suberror_Invalid_clean_aperture,
+ "Clean aperture cannot be applied to an image larger than 2^31 pixels in any direction");
}
- Fraction pcY = m_vertical_offset + Fraction(image_height - 1U, 2U);
- Fraction top = pcY - (m_clean_aperture_height - 1) / 2;
+ Fraction pcX = m_horizontal_offset + *halfWidth;
+ Fraction pcY = m_vertical_offset + *halfHeight;
- return top.round();
-}
+ Fraction left = pcX - (m_clean_aperture_width - 1) / 2;
+ Fraction top = pcY - (m_clean_aperture_height - 1) / 2;
-int Box_clap::bottom_rounded(uint32_t image_height) const
-{
- Fraction bottom = m_clean_aperture_height - 1 + top_rounded(image_height);
+ Crop crop;
+ crop.left = left.round_down();
+ crop.top = top.round();
+ crop.right = (m_clean_aperture_width - 1 + crop.left).round();
+ crop.bottom = (m_clean_aperture_height - 1 + crop.top).round();
- return bottom.round();
+ return crop;
}
int Box_clap::get_width_rounded() const
@@ -3781,17 +3788,32 @@ int Box_clap::get_height_rounded() const
return m_clean_aperture_height.round();
}
-void Box_clap::set(uint32_t clap_width, uint32_t clap_height,
- uint32_t image_width, uint32_t image_height)
+Error Box_clap::set(uint32_t clap_width, uint32_t clap_height,
+ uint32_t image_width, uint32_t image_height)
{
- assert(image_width >= clap_width);
- assert(image_height >= clap_height);
+ if (clap_width > image_width || clap_height > image_height) {
+ return Error(heif_error_Usage_error,
+ heif_suberror_Invalid_parameter_value,
+ "Clean aperture is larger than the image");
+ }
- m_clean_aperture_width = Fraction(clap_width, 1U);
- m_clean_aperture_height = Fraction(clap_height, 1U);
+ auto width = Fraction::from_unsigned(clap_width, 1);
+ auto height = Fraction::from_unsigned(clap_height, 1);
+ auto horizontal_offset = Fraction::from_signed(-int64_t{image_width - clap_width}, 2);
+ auto vertical_offset = Fraction::from_signed(-int64_t{image_height - clap_height}, 2);
- m_horizontal_offset = Fraction(-(int32_t) (image_width - clap_width), 2);
- m_vertical_offset = Fraction(-(int32_t) (image_height - clap_height), 2);
+ if (!width || !height || !horizontal_offset || !vertical_offset) {
+ return Error(heif_error_Usage_error,
+ heif_suberror_Invalid_parameter_value,
+ "Clean aperture values exceed the supported range");
+ }
+
+ m_clean_aperture_width = *width;
+ m_clean_aperture_height = *height;
+ m_horizontal_offset = *horizontal_offset;
+ m_vertical_offset = *vertical_offset;
+
+ return Error::Ok;
}
diff --git a/libheif/box.h b/libheif/box.h
index 5f77ce3d..35025ddb 100644
--- a/libheif/box.h
+++ b/libheif/box.h
@@ -65,10 +65,15 @@ class Fraction
public:
Fraction() = default;
- Fraction(int32_t num, int32_t den);
+ // Checked construction from externally supplied values (e.g. box fields or image
+ // sizes). Fails if a value does not fit into int32_t or if the denominator is zero.
+ static Result<Fraction> from_signed(int64_t num, int64_t den);
+
+ static Result<Fraction> from_unsigned(uint32_t num, uint32_t den);
- // may only use values up to int32_t maximum
- Fraction(uint32_t num, uint32_t den);
+ // The constructors are meant for arithmetic on values that are already known to be
+ // in range. They never fail: values are reduced in precision until they fit.
+ Fraction(int32_t num, int32_t den);
// Values will be reduced until they fit into int32_t.
Fraction(int64_t num, int64_t den);
@@ -1010,10 +1015,19 @@ public:
const char* debug_box_name() const override { return "Clean Aperture"; }
- int left_rounded(uint32_t image_width) const; // first column
- int right_rounded(uint32_t image_width) const; // last column that is part of the cropped image
- int top_rounded(uint32_t image_height) const; // first row
- int bottom_rounded(uint32_t image_height) const; // last row included in the cropped image
+ // The clean aperture in pixel coordinates of an image with the given size.
+ // 'right' and 'bottom' are the last column/row that are part of the cropped image.
+ struct Crop
+ {
+ int left = 0;
+ int top = 0;
+ int right = 0;
+ int bottom = 0;
+ };
+
+ // Fails if the clean aperture cannot be applied to an image of that size
+ // (zero size, or a size that exceeds the int32_t coordinate range).
+ Result<Crop> get_crop(uint32_t image_width, uint32_t image_height) const;
double left(int image_width) const;
double top(int image_height) const;
@@ -1022,8 +1036,10 @@ public:
int get_height_rounded() const;
- void set(uint32_t clap_width, uint32_t clap_height,
- uint32_t image_width, uint32_t image_height);
+ // Set a clean aperture of size clap_width x clap_height at the top-left corner of an
+ // image of size image_width x image_height.
+ Error set(uint32_t clap_width, uint32_t clap_height,
+ uint32_t image_width, uint32_t image_height);
[[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::ignorable; }
diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index b49ea391..b40a0252 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -309,7 +309,9 @@ Result<Encoder::CodedImageData> ImageItem::encode_to_bitstream_and_boxes(const s
input_height != encoded_height) {
auto clap = std::make_shared<Box_clap>();
- clap->set(input_width, input_height, encoded_width, encoded_height);
+ if (Error err = clap->set(input_width, input_height, encoded_width, encoded_height)) {
+ return err;
+ }
codedImage.properties.push_back(clap);
}
@@ -996,10 +998,15 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem::decode_image(const heif_decod
uint32_t img_width = img->get_width();
uint32_t img_height = img->get_height();
- int left = clap->left_rounded(img_width);
- int right = clap->right_rounded(img_width);
- int top = clap->top_rounded(img_height);
- int bottom = clap->bottom_rounded(img_height);
+ auto clapCrop = clap->get_crop(img_width, img_height);
+ if (!clapCrop) {
+ return clapCrop.error();
+ }
+
+ int left = clapCrop->left;
+ int right = clapCrop->right;
+ int top = clapCrop->top;
+ int bottom = clapCrop->bottom;
if (left < 0) { left = 0; }
if (top < 0) { top = 0; }
@@ -1338,7 +1345,7 @@ heif_image_tiling ImageItem::get_heif_image_tiling() const
// 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
+ // Box_clap::get_crop() (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;
@@ -1485,10 +1492,15 @@ Error ImageItem::process_image_transformations_on_tiling(heif_image_tiling& tili
if (auto clap = std::dynamic_pointer_cast<Box_clap>(property)) {
std::shared_ptr<HeifPixelImage> clap_img;
- int left = clap->left_rounded(tiling.image_width);
- int right = clap->right_rounded(tiling.image_width);
- int top = clap->top_rounded(tiling.image_height);
- int bottom = clap->bottom_rounded(tiling.image_height);
+ auto cropResult = clap->get_crop(tiling.image_width, tiling.image_height);
+ if (!cropResult) {
+ return cropResult.error();
+ }
+
+ int left = cropResult->left;
+ int right = cropResult->right;
+ int top = cropResult->top;
+ int bottom = cropResult->bottom;
if (left < 0) { left = 0; }
if (top < 0) { top = 0; }
diff --git a/libheif/region.cc b/libheif/region.cc
index 8db2dbce..5ae3d5e4 100644
--- a/libheif/region.cc
+++ b/libheif/region.cc
@@ -514,15 +514,15 @@ void RegionGeometry_InlineMask::encode(StreamWriter& writer, int field_size_byte
}
-RegionCoordinateTransform RegionCoordinateTransform::create(std::shared_ptr<HeifFile> file,
- heif_item_id item_id,
- int reference_width, int reference_height)
+Result<RegionCoordinateTransform> RegionCoordinateTransform::create(std::shared_ptr<HeifFile> file,
+ heif_item_id item_id,
+ int reference_width, int reference_height)
{
std::vector<std::shared_ptr<Box>> properties;
Error err = file->get_properties(item_id, properties);
if (err) {
- return {};
+ return RegionCoordinateTransform{};
}
uint32_t image_width = 0, image_height = 0;
@@ -536,7 +536,7 @@ RegionCoordinateTransform RegionCoordinateTransform::create(std::shared_ptr<Heif
}
if (image_width == 0 || image_height == 0) {
- return {};
+ return RegionCoordinateTransform{};
}
RegionCoordinateTransform transform;
@@ -598,10 +598,12 @@ RegionCoordinateTransform RegionCoordinateTransform::create(std::shared_ptr<Heif
}
case fourcc("clap"): {
auto clap = std::dynamic_pointer_cast<Box_clap>(property);
- int left = clap->left_rounded(image_width);
- int top = clap->top_rounded(image_height);
- transform.tx -= left;
- transform.ty -= top;
+ auto crop = clap->get_crop(image_width, image_height);
+ if (!crop) {
+ return crop.error();
+ }
+ transform.tx -= crop->left;
+ transform.ty -= crop->top;
image_width = clap->get_width_rounded();
image_height = clap->get_height_rounded();
break;
diff --git a/libheif/region.h b/libheif/region.h
index 34945b93..c5b0ac0e 100644
--- a/libheif/region.h
+++ b/libheif/region.h
@@ -188,9 +188,11 @@ class HeifFile;
class RegionCoordinateTransform
{
public:
- static RegionCoordinateTransform create(std::shared_ptr<HeifFile> file,
- heif_item_id item_id,
- int reference_width, int reference_height);
+ // Fails if a transformative property of the image cannot be applied (e.g. a 'clap'
+ // on an image size outside the supported range).
+ static Result<RegionCoordinateTransform> create(std::shared_ptr<HeifFile> file,
+ heif_item_id item_id,
+ int reference_width, int reference_height);
struct Point
{
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 144a6cae..8c45e4dc 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -39,6 +39,7 @@ else()
add_libheif_test(bitstream_tests)
add_libheif_test(box_equals)
add_libheif_test(clap_zero_size)
+ add_libheif_test(fraction)
add_libheif_test(conversion)
add_libheif_test(duplicate_alpha_channel)
add_libheif_test(idat)
diff --git a/tests/clap_zero_size.cc b/tests/clap_zero_size.cc
index eafc1258..abe817e7 100644
--- a/tests/clap_zero_size.cc
+++ b/tests/clap_zero_size.cc
@@ -26,17 +26,117 @@
#include "catch_amalgamated.hpp"
#include "box.h"
+#include "libheif/heif.h"
+#include "test_utils.h"
+#include <cstdlib>
-// 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)
+// Regression tests for GHSA-jc8f-p23p-5hjg and GHSA-gh5q-69gg-c964. A zero image
+// dimension used to underflow `image_width - 1U` to UINT32_MAX, and a dimension above
+// INT32_MAX + 1 exceeded the Fraction range directly. Both tripped an assert in the
+// former Fraction(uint32_t, uint32_t) constructor (abort in debug builds, corrupt crop
+// in release builds). Box_clap::get_crop() must report both as an error instead.
+
+static std::shared_ptr<Box_clap> make_clap()
+{
+ auto clap = std::make_shared<Box_clap>();
+ // clap 100x200 at the top-left corner of a 150x250 image
+ REQUIRE(clap->set(100, 200, 150, 250).error_code == heif_error_Ok);
+ return clap;
+}
+
+TEST_CASE("clap crop for a valid image size") {
+ auto clap = make_clap();
+
+ auto crop = clap->get_crop(150, 250);
+ REQUIRE(crop);
+ REQUIRE(crop->left == 0);
+ REQUIRE(crop->right == 99);
+ REQUIRE(crop->top == 0);
+ REQUIRE(crop->bottom == 199);
+}
+
+TEST_CASE("clap crop with zero image size") {
+ auto clap = make_clap();
+
+ const uint32_t sizes[][2] = {{0, 250}, {150, 0}, {0, 0}};
+ for (auto& size : sizes) {
+ auto crop = clap->get_crop(size[0], size[1]);
+ REQUIRE(!crop);
+ REQUIRE(crop.error().error_code == heif_error_Invalid_input);
+ REQUIRE(crop.error().sub_error_code == heif_suberror_Invalid_clean_aperture);
+ }
+}
+
+TEST_CASE("clap crop with oversized image size") {
+ auto clap = make_clap();
+
+ const uint32_t sizes[][2] = {{0xFFFFFFFF, 250}, {150, 0xFFFFFFFF}, {0x80000001, 250}, {150, 0x80000001}};
+ for (auto& size : sizes) {
+ auto crop = clap->get_crop(size[0], size[1]);
+ REQUIRE(!crop);
+ REQUIRE(crop.error().error_code == heif_error_Invalid_input);
+ REQUIRE(crop.error().sub_error_code == heif_suberror_Invalid_clean_aperture);
+ }
+
+ // The largest values that still fit must be computed normally: the crop is centered
+ // (up to the Fraction's reduced precision at this magnitude) and keeps its size.
+ auto crop = clap->get_crop(0x80000000, 0x80000000);
+ REQUIRE(crop);
+ REQUIRE(std::abs(crop->left - (0x40000000 - 75)) <= 1);
+ REQUIRE(crop->right - crop->left + 1 == 100);
+ REQUIRE(std::abs(crop->top - (0x40000000 - 125)) <= 1);
+ REQUIRE(crop->bottom - crop->top + 1 == 200);
+}
+
+TEST_CASE("clap set() rejects a clean aperture larger than the image") {
+ auto clap = std::make_shared<Box_clap>();
+ REQUIRE(clap->set(200, 100, 150, 250).error_code == heif_error_Usage_error);
+ REQUIRE(clap->set(100, 300, 150, 250).error_code == heif_error_Usage_error);
+}
+
+
+// The same bug through the public API. The files carry an 'ispe' with one dimension of
+// 0xFFFFFFFF and a 'clap' property; two of them add an 'irot' so that the oversized
+// dimension arrives at the other side of the clap computation after the rotation swaps
+// width and height.
+TEST_CASE("image tiling with clap and oversized ispe") {
+ const char* files[] = {
+ "clap_oversized_ispe_height.avif",
+ "clap_oversized_ispe_width.avif",
+ "clap_oversized_ispe_irot180.avif",
+ "clap_oversized_ispe_irot90.avif",
+ };
+
+ for (const char* file : files) {
+ INFO(file);
+
+ heif_context* ctx = get_context_for_test_file(file);
+ heif_image_handle* handle = get_primary_image_handle(ctx);
+ heif_image_tiling tiling{};
+
+ // With the default security limits, the tiling API must reject the image like the
+ // decoding path does, instead of handing out a size nobody can allocate.
+ heif_error err = heif_image_handle_get_image_tiling(handle, 1, &tiling);
+ REQUIRE(err.code == heif_error_Memory_allocation_error);
+ REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded);
+
+ // Without limits, the clap computation itself must report the error instead of
+ // aborting on an assert or computing a bogus crop.
+ heif_context_set_security_limits(ctx, heif_get_disabled_security_limits());
+ err = heif_image_handle_get_image_tiling(handle, 1, &tiling);
+ REQUIRE(err.code == heif_error_Invalid_input);
+ REQUIRE(err.subcode == heif_suberror_Invalid_clean_aperture);
+
+ // The crop-border query has no error return and reports "no cropping" instead.
+ int left = -1, top = -1, right = -1, bottom = -1;
+ heif_item_get_property_transform_crop_borders(ctx, heif_image_handle_get_item_id(handle), 0,
+ 64, -1, &left, &top, &right, &bottom);
+ REQUIRE(left == 0);
+ REQUIRE(top == 0);
+ REQUIRE(right == 0);
+ REQUIRE(bottom == 0);
+
+ heif_image_handle_release(handle);
+ heif_context_free(ctx);
+ }
}
diff --git a/tests/data/clap_oversized_ispe_height.avif b/tests/data/clap_oversized_ispe_height.avif
new file mode 100644
index 00000000..a570e097
Binary files /dev/null and b/tests/data/clap_oversized_ispe_height.avif differ
diff --git a/tests/data/clap_oversized_ispe_irot180.avif b/tests/data/clap_oversized_ispe_irot180.avif
new file mode 100644
index 00000000..417e947b
Binary files /dev/null and b/tests/data/clap_oversized_ispe_irot180.avif differ
diff --git a/tests/data/clap_oversized_ispe_irot90.avif b/tests/data/clap_oversized_ispe_irot90.avif
new file mode 100644
index 00000000..412cbddd
Binary files /dev/null and b/tests/data/clap_oversized_ispe_irot90.avif differ
diff --git a/tests/data/clap_oversized_ispe_width.avif b/tests/data/clap_oversized_ispe_width.avif
new file mode 100644
index 00000000..d487bef0
Binary files /dev/null and b/tests/data/clap_oversized_ispe_width.avif differ
diff --git a/tests/fraction.cc b/tests/fraction.cc
new file mode 100644
index 00000000..453fa854
--- /dev/null
+++ b/tests/fraction.cc
@@ -0,0 +1,71 @@
+/*
+ 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"
+#include <cmath>
+#include <limits>
+
+// The checked Fraction factories replace the former Fraction(uint32_t, uint32_t)
+// constructor, which only assert()ed its value range (GHSA-gh5q-69gg-c964).
+
+TEST_CASE("Fraction::from_unsigned") {
+ auto f = Fraction::from_unsigned(0x7FFFFFFF, 2);
+ REQUIRE(f);
+ REQUIRE(std::abs(f->to_double() - 0x7FFFFFFF / 2.0) < 1.0);
+
+ REQUIRE(Fraction::from_unsigned(0, 1));
+ REQUIRE(Fraction::from_unsigned(0x7FFFFFFF, 0x7FFFFFFF));
+
+ REQUIRE(!Fraction::from_unsigned(0x80000000, 1));
+ REQUIRE(!Fraction::from_unsigned(1, 0x80000000));
+ REQUIRE(!Fraction::from_unsigned(0xFFFFFFFF, 2));
+ REQUIRE(!Fraction::from_unsigned(1, 0));
+}
+
+TEST_CASE("Fraction::from_signed") {
+ const int64_t max = std::numeric_limits<int32_t>::max();
+ const int64_t min = std::numeric_limits<int32_t>::min();
+
+ auto f = Fraction::from_signed(-5, 3);
+ REQUIRE(f);
+ REQUIRE(f->numerator == -5);
+ REQUIRE(f->denominator == 3);
+
+ REQUIRE(Fraction::from_signed(max, 1));
+ REQUIRE(Fraction::from_signed(min, 1));
+ REQUIRE(Fraction::from_signed(1, max));
+
+ REQUIRE(!Fraction::from_signed(max + 1, 1));
+ REQUIRE(!Fraction::from_signed(min - 1, 1));
+ REQUIRE(!Fraction::from_signed(1, max + 1));
+ REQUIRE(!Fraction::from_signed(1, min - 1));
+
+ auto zero_den = Fraction::from_signed(1, 0);
+ REQUIRE(!zero_den);
+ REQUIRE(zero_den.error().error_code == heif_error_Invalid_input);
+ REQUIRE(zero_den.error().sub_error_code == heif_suberror_Invalid_fractional_number);
+}