Commit 942e4095 for libheif
commit 942e40954389e0d705a0f2f6818d5f4b5c8f7c78
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Fri May 15 21:05:48 2026 +0200
validate JPEG-2000 component sizes (#1796)
diff --git a/libheif/image/pixelimage.cc b/libheif/image/pixelimage.cc
index d5de201a..43eaddcf 100644
--- a/libheif/image/pixelimage.cc
+++ b/libheif/image/pixelimage.cc
@@ -731,30 +731,44 @@ uint32_t HeifPixelImage::get_height(uint32_t component_id) const
bool HeifPixelImage::primary_planes_have_size(uint32_t width, uint32_t height) const
{
- auto channel_has_size = [&](heif_channel channel) {
+ auto channel_has_size = [&](heif_channel channel, uint32_t w, uint32_t h) {
// get_width()/get_height() return 0 for an absent channel -> mismatch.
- return get_width(channel) == width && get_height(channel) == height;
+ return get_width(channel) == w && get_height(channel) == h;
};
switch (m_colorspace) {
case heif_colorspace_monochrome:
- case heif_colorspace_YCbCr:
- // Cb/Cr may be legitimately subsampled, so only the Y plane is checked.
- return channel_has_size(heif_channel_Y);
+ return channel_has_size(heif_channel_Y, width, height);
+
+ case heif_colorspace_YCbCr: {
+ // Y has the full size; Cb/Cr are subsampled according to the chroma format.
+ // Downstream color conversion derives chroma plane indices from m_chroma, so
+ // Cb/Cr must actually match those subsampled dimensions (issue #1796).
+ if (!channel_has_size(heif_channel_Y, width, height)) {
+ return false;
+ }
+ if (m_chroma == heif_chroma_monochrome) {
+ return true;
+ }
+ uint32_t chroma_w, chroma_h;
+ get_subsampled_size(width, height, heif_channel_Cb, m_chroma, &chroma_w, &chroma_h);
+ return channel_has_size(heif_channel_Cb, chroma_w, chroma_h) &&
+ channel_has_size(heif_channel_Cr, chroma_w, chroma_h);
+ }
case heif_colorspace_RGB:
if (m_chroma == heif_chroma_444) {
// planar RGB: all three planes must be present and have the full size
- return channel_has_size(heif_channel_R) &&
- channel_has_size(heif_channel_G) &&
- channel_has_size(heif_channel_B);
+ return channel_has_size(heif_channel_R, width, height) &&
+ channel_has_size(heif_channel_G, width, height) &&
+ channel_has_size(heif_channel_B, width, height);
}
else {
- return channel_has_size(heif_channel_interleaved);
+ return channel_has_size(heif_channel_interleaved, width, height);
}
case heif_colorspace_filter_array:
- return channel_has_size(heif_channel_filter_array);
+ return channel_has_size(heif_channel_filter_array, width, height);
case heif_colorspace_undefined:
default:
diff --git a/libheif/plugins/decoder_openjpeg.cc b/libheif/plugins/decoder_openjpeg.cc
index 3f94ed14..63a021f0 100644
--- a/libheif/plugins/decoder_openjpeg.cc
+++ b/libheif/plugins/decoder_openjpeg.cc
@@ -22,6 +22,7 @@
#include "libheif/heif.h"
#include "libheif/heif_plugin.h"
#include "decoder_openjpeg.h"
+#include "common_utils.h"
#include <openjpeg.h>
#include <cstring>
@@ -435,6 +436,20 @@ heif_error openjpeg_decode_next_image2(void* decoder_raw, heif_image** out_img,
}
+ // Validate per-component sizes against the chroma format derived above. A malformed
+ // JPEG 2000 stream may set comp[1].dx/dy consistently with a chroma format yet declare
+ // comp[c].w/h that do not match the subsampled dimensions; using such planes downstream
+ // causes out-of-bounds reads in color conversion (issue #1796).
+ for (size_t c = 0; c < image->numcomps; c++) {
+ uint32_t expected_w, expected_h;
+ get_subsampled_size(static_cast<uint32_t>(width), static_cast<uint32_t>(height),
+ channels[c], chroma, &expected_w, &expected_h);
+ if (image->comps[c].w != expected_w || image->comps[c].h != expected_h) {
+ return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified,
+ "JPEG 2000 component size does not match the image's chroma subsampling"};
+ }
+ }
+
heif_error error = heif_image_create(width, height, colorspace, chroma, out_img);
if (error.code) {
return error;