Commit 3a6e1cf4 for libheif

commit 3a6e1cf41622882b3c9f844ba1d966cd082bc26c
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Thu Jun 25 21:23:35 2026 +0200

    Reject 16-bit interleaved channels with bit depth <= 8 (GHSA-46rp-pcq2-rpmr)

    The RRGGBB(AA) interleaved chroma formats store each component as 16 bit.
    Creating such a channel with a bit depth of <= 8 was self-inconsistent: the
    plane was allocated at one byte per component while the uncompressed encoder
    reads/writes the samples as 16-bit values. This led to a heap out-of-bounds
    write (and over-read) in the uncompressed RGB encoder.

    Catch this at the construction chokepoint in HeifPixelImage::add_channel()
    so the malformed image can no longer exist, instead of guarding each encoder.

    Add tests covering the rejection and the encoder boundary bit depths.

diff --git a/libheif/image/pixelimage.cc b/libheif/image/pixelimage.cc
index 6a357762..b2201ff7 100644
--- a/libheif/image/pixelimage.cc
+++ b/libheif/image/pixelimage.cc
@@ -358,6 +358,19 @@ Error HeifPixelImage::add_channel(heif_channel channel, uint32_t width, uint32_t
     bit_depth = 8;
   }

+  // The RRGGBB(AA) interleaved formats store each component as 16 bit. A bit depth
+  // of <= 8 would be self-inconsistent: the allocated plane would only hold one byte
+  // per component while readers/writers access the samples as 16-bit values.
+  if ((m_chroma == heif_chroma_interleaved_RRGGBB_BE ||
+       m_chroma == heif_chroma_interleaved_RRGGBB_LE ||
+       m_chroma == heif_chroma_interleaved_RRGGBBAA_BE ||
+       m_chroma == heif_chroma_interleaved_RRGGBBAA_LE) &&
+      bit_depth <= 8) {
+    return {heif_error_Usage_error,
+            heif_suberror_Unspecified,
+            "Cannot create a 16-bit interleaved channel with a bit depth of 8 or less"};
+  }
+
   int num_interleaved_pixels = num_interleaved_components_per_plane(m_chroma);

   ComponentStorage plane;
diff --git a/tests/uncompressed_encode.cc b/tests/uncompressed_encode.cc
index 196cda91..8e4b2873 100644
--- a/tests/uncompressed_encode.cc
+++ b/tests/uncompressed_encode.cc
@@ -711,6 +711,73 @@ TEST_CASE("Encode RRRGGBB_BE 16 bit ")
 }


+// Boundary cases for the RRGGBB block-pixel encoder. 9 bit is the lowest valid
+// depth (m_bytes_per_pixel becomes 4); 13 bit is the highest the block encoder
+// accepts (m_bytes_per_pixel == 5). Both were adjacent to the out-of-bounds write
+// that occurred for the (now rejected) <= 8 bit case (GHSA-46rp-pcq2-rpmr).
+TEST_CASE("Encode RRRGGBB_LE 9 bit")
+{
+  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_LE, 9, true, false);
+  do_encode(input_image, "encode_rrggbb_9_le.heif", false);
+}
+
+
+TEST_CASE("Encode RRRGGBB_BE 9 bit")
+{
+  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_BE, 9, false, false);
+  do_encode(input_image, "encode_rrggbb_9_be.heif", false);
+}
+
+
+TEST_CASE("Encode RRRGGBB_LE 13 bit")
+{
+  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_LE, 13, true, false);
+  do_encode(input_image, "encode_rrggbb_13_le.heif", false);
+}
+
+
+TEST_CASE("Encode RRRGGBB_BE 13 bit")
+{
+  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_BE, 13, false, false);
+  do_encode(input_image, "encode_rrggbb_13_be.heif", false);
+}
+
+
+// Root-cause check: a 16-bit interleaved channel (RRGGBB / RRGGBBAA) with a bit
+// depth of <= 8 is self-inconsistent and must be rejected at image construction,
+// before any encoder can read/write past the under-sized plane.
+TEST_CASE("Reject <=8 bit 16-bit-interleaved channels")
+{
+  const heif_chroma formats[] = {
+    heif_chroma_interleaved_RRGGBB_LE,
+    heif_chroma_interleaved_RRGGBB_BE,
+    heif_chroma_interleaved_RRGGBBAA_LE,
+    heif_chroma_interleaved_RRGGBBAA_BE
+  };
+
+  for (heif_chroma chroma : formats) {
+    for (int bit_depth = 1; bit_depth <= 8; bit_depth++) {
+      heif_image* image;
+      heif_error err = heif_image_create(64, 64, heif_colorspace_RGB, chroma, &image);
+      REQUIRE(err.code == heif_error_Ok);
+
+      err = heif_image_add_plane(image, heif_channel_interleaved, 64, 64, bit_depth);
+      REQUIRE(err.code != heif_error_Ok);
+
+      heif_image_release(image);
+    }
+
+    // The lowest valid depth (9 bit) is still accepted.
+    heif_image* image;
+    heif_error err = heif_image_create(64, 64, heif_colorspace_RGB, chroma, &image);
+    REQUIRE(err.code == heif_error_Ok);
+    err = heif_image_add_plane(image, heif_channel_interleaved, 64, 64, 9);
+    REQUIRE(err.code == heif_error_Ok);
+    heif_image_release(image);
+  }
+}
+
+
 TEST_CASE("Encode RGBA")
 {
   heif_image *input_image = createImage_RGBA_interleaved();