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();