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;