Commit ac5521ad for libheif
commit ac5521ad50399885de96bb6a0733a5d2442740f9
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Thu Jun 25 20:29:50 2026 +0200
Reject uncompressed encode of images with mismatched component plane sizes (GHSA-xpw3-9rhw-482x)
The uncompressed encoders size their output buffer from the primary image
dimensions but copy each component plane using that plane's actual size. A
component plane larger than the primary image (e.g. an alpha plane that does
not match the color planes) was memcpy'd past the end of the buffer, an
attacker-controlled heap out-of-bounds write.
Validate component plane sizes against the primary dimensions at the start of
unc_encoder::encode(), which covers all uncompressed encoder variants. Chroma
(Cb/Cr) planes are checked against the subsampled size; all other planes must
match the full image size. Inconsistent images are now rejected with an error
instead of overflowing the heap.
Add a regression test that builds a 2x2 image with a 256x256 alpha plane and
checks that encoding fails cleanly.
diff --git a/libheif/codecs/uncompressed/unc_encoder.cc b/libheif/codecs/uncompressed/unc_encoder.cc
index 8d33763a..936075e5 100644
--- a/libheif/codecs/uncompressed/unc_encoder.cc
+++ b/libheif/codecs/uncompressed/unc_encoder.cc
@@ -185,9 +185,52 @@ heif_uncompressed_component_format to_unc_component_format(const std::shared_ptr
}
+Error unc_encoder::check_component_sizes(const std::shared_ptr<const HeifPixelImage>& src_image)
+{
+ uint32_t image_width = src_image->get_width();
+ uint32_t image_height = src_image->get_height();
+ heif_chroma chroma = src_image->get_chroma_format();
+
+ for (uint32_t id : src_image->get_used_planar_component_ids()) {
+ heif_channel channel = src_image->get_component_channel(id);
+
+ uint32_t expected_width = image_width;
+ uint32_t expected_height = image_height;
+
+ if (channel == heif_channel_Cb || channel == heif_channel_Cr) {
+ if (chroma == heif_chroma_420) {
+ expected_width = (image_width + 1) / 2;
+ expected_height = (image_height + 1) / 2;
+ }
+ else if (chroma == heif_chroma_422) {
+ expected_width = (image_width + 1) / 2;
+ }
+ }
+
+ if (src_image->get_component_width(id) != expected_width ||
+ src_image->get_component_height(id) != expected_height) {
+ return {heif_error_Invalid_input,
+ heif_suberror_Unspecified,
+ "Image component plane size does not match the image dimensions"};
+ }
+ }
+
+ return Error::Ok;
+}
+
+
Result<Encoder::CodedImageData> unc_encoder::encode(const std::shared_ptr<const HeifPixelImage>& src_image,
const heif_encoding_options& in_options) const
{
+ // The encoders size their output buffer from the primary image dimensions, but copy each
+ // component plane using that plane's actual size. If a component plane is larger than the primary
+ // image (e.g. an alpha plane that does not match the color planes), this writes out of bounds.
+ // Reject any image whose component planes are inconsistent with the primary size. Chroma (Cb/Cr)
+ // planes are legitimately subsampled, so they are checked against the subsampled size.
+ if (Error err = check_component_sizes(src_image)) {
+ return err;
+ }
+
auto parameters = std::unique_ptr<heif_unci_image_parameters,
void (*)(heif_unci_image_parameters*)>(heif_unci_image_parameters_alloc(),
heif_unci_image_parameters_release);
diff --git a/libheif/codecs/uncompressed/unc_encoder.h b/libheif/codecs/uncompressed/unc_encoder.h
index 3ef245f7..5845b77c 100644
--- a/libheif/codecs/uncompressed/unc_encoder.h
+++ b/libheif/codecs/uncompressed/unc_encoder.h
@@ -69,6 +69,11 @@ public:
Result<Encoder::CodedImageData> encode(const std::shared_ptr<const HeifPixelImage>& src_image,
const heif_encoding_options& options) const;
+ // Verify that every component plane has the size implied by the primary image dimensions
+ // (chroma planes may be subsampled). The encoders assume this when sizing their output buffer,
+ // so a mismatched plane would otherwise overflow the buffer during encoding.
+ static Error check_component_sizes(const std::shared_ptr<const HeifPixelImage>& src_image);
+
protected:
std::shared_ptr<Box_cmpd> m_cmpd;
std::shared_ptr<Box_uncC> m_uncC;
diff --git a/tests/uncompressed_encode.cc b/tests/uncompressed_encode.cc
index 4b6587e0..196cda91 100644
--- a/tests/uncompressed_encode.cc
+++ b/tests/uncompressed_encode.cc
@@ -955,3 +955,46 @@ TEST_CASE("Encode tiled YCbCr 4:2:0 unci - chroma placement round-trip")
heif_image_release(input_image);
heif_context_free(ctx);
}
+
+
+// Regression test for the heap OOB write reported as GHSA-xpw3-9rhw-482x
+// (https://github.com/strukturag/libheif/security/advisories/GHSA-xpw3-9rhw-482x).
+// The uncompressed encoder sized its output buffer from the primary image
+// dimensions but copied each component plane using that plane's actual size.
+// A component plane larger than the primary image (e.g. an alpha plane that
+// does not match the color planes, as produced by an image sequence with a
+// mismatched alpha auxiliary track) was memcpy'd past the end of the buffer.
+// The encoder now rejects such inconsistent images instead of overflowing.
+TEST_CASE("Encode rejects oversized component plane")
+{
+ heif_image *image;
+ heif_error err = heif_image_create(2, 2, heif_colorspace_monochrome,
+ heif_chroma_monochrome, &image);
+ REQUIRE(err.code == heif_error_Ok);
+
+ err = heif_image_add_plane(image, heif_channel_Y, 2, 2, 8);
+ REQUIRE(err.code == heif_error_Ok);
+
+ // Alpha plane much larger than the 2x2 primary image. heif_image_add_plane
+ // does not validate the plane size against the image size, so this builds the
+ // same inconsistent image that the sequence decoder used to produce.
+ err = heif_image_add_plane(image, heif_channel_Alpha, 256, 256, 8);
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_context *ctx = heif_context_alloc();
+ heif_encoder *encoder;
+ err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder);
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_image_handle *output_image_handle = nullptr;
+ err = heif_context_encode_image(ctx, image, encoder, nullptr, &output_image_handle);
+ // Must fail cleanly rather than overflow the heap.
+ REQUIRE(err.code != heif_error_Ok);
+
+ if (output_image_handle) {
+ heif_image_handle_release(output_image_handle);
+ }
+ heif_encoder_release(encoder);
+ heif_image_release(image);
+ heif_context_free(ctx);
+}