Commit 91dd5176 for libheif
commit 91dd51767c51a25018580b0254b0a1e2e2b381b7
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Wed Apr 29 16:07:04 2026 +0200
OpenJPEG decoder: check image to be decoded against libheif security limits (#1774)
diff --git a/libheif/plugins/decoder_openjpeg.cc b/libheif/plugins/decoder_openjpeg.cc
index 6d44f5d3..3d294c90 100644
--- a/libheif/plugins/decoder_openjpeg.cc
+++ b/libheif/plugins/decoder_openjpeg.cc
@@ -276,6 +276,36 @@ opj_stream_t* opj_stream_create_default_memory_stream(openjpeg_decoder* p_decode
//**************************************************************************
+// Conservative upper bound on bytes OpenJPEG will allocate to decode this
+// codestream. Saturates to UINT64_MAX on overflow. OpenJPEG stores each sample
+// internally as OPJ_INT32 regardless of the codestream bit depth; the 3x
+// multiplier covers the final image planes plus in-flight tile and DWT
+// working buffers.
+static uint64_t openjpeg_estimate_decode_memory_bytes(const opj_image_t* image)
+{
+ auto sat_mul = [](uint64_t a, uint64_t b) -> uint64_t {
+ if (a == 0 || b == 0) return 0;
+ if (a > UINT64_MAX / b) return UINT64_MAX;
+ return a * b;
+ };
+
+ auto sat_add = [](uint64_t a, uint64_t b) -> uint64_t {
+ uint64_t s = a + b;
+ return (s < a) ? UINT64_MAX : s;
+ };
+
+ uint64_t total = 0;
+ for (uint32_t c = 0; c < image->numcomps; c++) {
+ const opj_image_comp_t& comp = image->comps[c];
+ uint64_t plane = sat_mul(uint64_t(comp.w), uint64_t(comp.h));
+ plane = sat_mul(plane, sizeof(OPJ_INT32));
+ total = sat_add(total, plane);
+ }
+
+ return sat_mul(total, 3);
+}
+
+
heif_error openjpeg_decode_next_image2(void* decoder_raw, heif_image** out_img,
uintptr_t* out_user_data,
const heif_security_limits* limits)
@@ -317,6 +347,32 @@ heif_error openjpeg_decode_next_image2(void* decoder_raw, heif_image** out_img,
std::unique_ptr<opj_image_t, void (OPJ_CALLCONV *)(opj_image_t*)> image(image_ptr, opj_image_destroy);
+ // Reject obvious memory bombs before letting OpenJPEG allocate decode buffers.
+ // OpenJPEG has no built-in resource limit API, so we enforce libheif's limits here.
+ if (image->x1 < image->x0 || image->y1 < image->y0) {
+ return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified,
+ "Invalid JPEG 2000 image bounding box"};
+ }
+
+ uint64_t img_w = image->x1 - image->x0;
+ uint64_t img_h = image->y1 - image->y0;
+
+ // image->x0,x1,y0,y1 are uint32, thus no overflow here
+ uint64_t pixels = img_w * img_h;
+ if (limits->max_image_size_pixels > 0 && pixels > limits->max_image_size_pixels) {
+ return {heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded,
+ "JPEG 2000 image exceeds maximum allowed image size"};
+ }
+
+ uint64_t estimated_memory = openjpeg_estimate_decode_memory_bytes(image.get());
+ if (limits->max_memory_block_size > 0 && estimated_memory > limits->max_memory_block_size) {
+ return {heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded,
+ "JPEG 2000 image would require too much memory to decode"};
+ }
+
+ // TODO: also enforce limits->max_components against image->numcomps, and
+ // limits->max_number_of_tiles against opj_get_cstr_info()->tw * th.
+
if (image->numcomps != 3 && image->numcomps != 1) {
//TODO - Handle other numbers of components
return {heif_error_Unsupported_feature, heif_suberror_Unsupported_data_version, "Number of components must be 3 or 1"};