Commit 62d60530 for libheif
commit 62d60530610110fc7bc6d08ff30f2cf23917a1eb
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon May 18 01:41:34 2026 +0200
unci: fix heap OOB in tiled chroma decode (GHSA-5x55-x5pf-9c6g)
diff --git a/libheif/codecs/uncompressed/unc_decoder_component_interleave.cc b/libheif/codecs/uncompressed/unc_decoder_component_interleave.cc
index d94f6bdb..8e5c1da4 100644
--- a/libheif/codecs/uncompressed/unc_decoder_component_interleave.cc
+++ b/libheif/codecs/uncompressed/unc_decoder_component_interleave.cc
@@ -96,13 +96,23 @@ Error unc_decoder_component_interleave::decode_tile(const std::vector<uint8_t>&
bool per_channel_tile_align = (m_uncC->get_interleave_type() == interleave_mode_tile_component);
+ // out_x0/out_y0 are in full-resolution image coordinates. For subsampled channels
+ // (Cb/Cr at 4:2:0 or 4:2:2), entry.tile_width/tile_height and entry.dst_plane_stride
+ // refer to the subsampled chroma plane, so the destination origin must be scaled
+ // to the channel's grid. Failing to do so wrote past the chroma plane on any
+ // non-first tile row/column — see GHSA-5x55-x5pf-9c6g.
+ uint32_t tile_col = out_x0 / m_tile_width;
+ uint32_t tile_row = out_y0 / m_tile_height;
+
for (ChannelListEntry& entry : channelList) {
srcBits.markTileStart();
+ uint64_t channel_x0 = uint64_t{tile_col} * entry.tile_width;
+ uint64_t channel_y0 = uint64_t{tile_row} * entry.tile_height;
for (uint32_t y = 0; y < entry.tile_height; y++) {
srcBits.markRowStart();
if (entry.use_channel) {
- uint64_t dst_row_offset = uint64_t{(out_y0 + y)} * entry.dst_plane_stride;
- processComponentTileRow(entry, srcBits, dst_row_offset + out_x0 * entry.bytes_per_component_sample);
+ uint64_t dst_row_offset = (channel_y0 + y) * entry.dst_plane_stride;
+ processComponentTileRow(entry, srcBits, dst_row_offset + channel_x0 * entry.bytes_per_component_sample);
}
else {
srcBits.skip_bytes(entry.bytes_per_tile_row_src);
diff --git a/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc b/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc
index f21199fe..81223107 100644
--- a/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc
+++ b/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc
@@ -74,17 +74,27 @@ Error unc_decoder_mixed_interleave::decode_tile(const std::vector<uint8_t>& tile
void unc_decoder_mixed_interleave::processTile(UncompressedBitReader& srcBits, uint32_t out_x0, uint32_t out_y0)
{
+ // out_x0/out_y0 are in full-resolution image coordinates. For subsampled chroma
+ // channels (4:2:0 or 4:2:2), entry.tile_width/tile_height and entry.dst_plane_stride
+ // refer to the subsampled chroma plane, so the destination origin must be scaled
+ // to each channel's grid. Failing to do so wrote past the chroma plane on any
+ // non-first tile row/column — see GHSA-5x55-x5pf-9c6g.
+ uint32_t tile_col = out_x0 / m_tile_width;
+ uint32_t tile_row = out_y0 / m_tile_height;
+
bool haveProcessedChromaForThisTile = false;
for (ChannelListEntry& entry : channelList) {
if (entry.use_channel) {
+ uint64_t channel_x0 = uint64_t{tile_col} * entry.tile_width;
+ uint64_t channel_y0 = uint64_t{tile_row} * entry.tile_height;
if ((entry.channel == heif_channel_Cb) || (entry.channel == heif_channel_Cr)) {
if (!haveProcessedChromaForThisTile) {
for (uint32_t tile_y = 0; tile_y < entry.tile_height; tile_y++) {
// TODO: row padding
- uint64_t dst_row_number = tile_y + out_y0;
+ uint64_t dst_row_number = tile_y + channel_y0;
uint64_t dst_row_offset = dst_row_number * entry.dst_plane_stride;
for (uint32_t tile_x = 0; tile_x < entry.tile_width; tile_x++) {
- uint64_t dst_column_number = out_x0 + tile_x;
+ uint64_t dst_column_number = channel_x0 + tile_x;
uint64_t dst_column_offset = dst_column_number * entry.bytes_per_component_sample;
int val = srcBits.get_bits(entry.bytes_per_component_sample * 8);
memcpy_to_native_endian(entry.dst_plane + dst_row_offset + dst_column_offset, val, entry.bytes_per_component_sample);
@@ -99,8 +109,8 @@ void unc_decoder_mixed_interleave::processTile(UncompressedBitReader& srcBits, u
}
else {
for (uint32_t tile_y = 0; tile_y < entry.tile_height; tile_y++) {
- uint64_t dst_row_offset = uint64_t{(out_y0 + tile_y)} * entry.dst_plane_stride;
- processComponentTileRow(entry, srcBits, dst_row_offset + out_x0 * entry.bytes_per_component_sample);
+ uint64_t dst_row_offset = (channel_y0 + tile_y) * entry.dst_plane_stride;
+ processComponentTileRow(entry, srcBits, dst_row_offset + channel_x0 * entry.bytes_per_component_sample);
}
}
}
diff --git a/tests/uncompressed_encode.cc b/tests/uncompressed_encode.cc
index 8aaee692..4b6587e0 100644
--- a/tests/uncompressed_encode.cc
+++ b/tests/uncompressed_encode.cc
@@ -27,6 +27,8 @@
#include "catch_amalgamated.hpp"
#include "api_structs.h"
#include "libheif/heif.h"
+#include "libheif/heif_uncompressed.h"
+#include "libheif/heif_tiling.h"
#include <cstdint>
#include <string.h>
#include "test_utils.h"
@@ -776,3 +778,180 @@ TEST_CASE("Encode RGBA planar")
heif_image *input_image = createImage_RGBA_planar();
do_encode(input_image, "encode_rgba_planar.heif", true);
}
+
+
+// Regression test for the heap OOB write reported as GHSA-5x55-x5pf-9c6g
+// (https://github.com/strukturag/libheif/security/advisories/GHSA-5x55-x5pf-9c6g).
+// Chroma destination offsets in unc_decoder_component_interleave and
+// unc_decoder_mixed_interleave used the full-resolution tile origin against
+// the subsampled chroma plane stride, so the second tile row in a 4:2:0 tiled
+// unci image was written past the end of the chroma plane. The fix scales the
+// tile origin into the chroma grid; this test encodes a 2x2-tiled YCbCr 4:2:0
+// unci with a distinctive per-tile chroma pattern and verifies the round-trip
+// places each tile correctly.
+static heif_image *createImage_YCbCr_420_tiled_pattern(int w, int h)
+{
+ heif_image *image;
+ heif_error err;
+ err = heif_image_create(w, h, heif_colorspace_YCbCr, heif_chroma_420, &image);
+ REQUIRE(err.code == heif_error_Ok);
+
+ err = heif_image_add_plane(image, heif_channel_Y, w, h, 8);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_image_add_plane(image, heif_channel_Cb, w / 2, h / 2, 8);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_image_add_plane(image, heif_channel_Cr, w / 2, h / 2, 8);
+ REQUIRE(err.code == heif_error_Ok);
+
+ int y_stride, cb_stride, cr_stride;
+ uint8_t *Y = heif_image_get_plane(image, heif_channel_Y, &y_stride);
+ uint8_t *Cb = heif_image_get_plane(image, heif_channel_Cb, &cb_stride);
+ uint8_t *Cr = heif_image_get_plane(image, heif_channel_Cr, &cr_stride);
+
+ // Each tile (in a 2x2 tile grid) gets a unique chroma constant so that
+ // misplaced chroma tiles are detected.
+ for (int row = 0; row < h; row++) {
+ for (int col = 0; col < w; col++) {
+ Y[row * y_stride + col] = static_cast<uint8_t>((row * 3 + col) & 0xFF);
+ }
+ }
+ int cw = w / 2;
+ int ch = h / 2;
+ for (int row = 0; row < ch; row++) {
+ int tile_row = (row < ch / 2) ? 0 : 1;
+ for (int col = 0; col < cw; col++) {
+ int tile_col = (col < cw / 2) ? 0 : 1;
+ // Distinct per-tile chroma constants.
+ Cb[row * cb_stride + col] = static_cast<uint8_t>(0x10 + tile_row * 2 + tile_col);
+ Cr[row * cr_stride + col] = static_cast<uint8_t>(0xA0 + tile_row * 2 + tile_col);
+ }
+ }
+
+ return image;
+}
+
+TEST_CASE("Encode tiled YCbCr 4:2:0 unci - chroma placement round-trip")
+{
+ const int W = 32;
+ const int H = 32;
+ const int TW = 16;
+ const int TH = 16;
+
+ heif_unci_image_parameters params{};
+ params.version = 1;
+ params.image_width = W;
+ params.image_height = H;
+ params.tile_width = TW;
+ params.tile_height = TH;
+ params.compression = heif_unci_compression_off;
+
+ heif_image *input_image = createImage_YCbCr_420_tiled_pattern(W, H);
+ REQUIRE(input_image != nullptr);
+
+ heif_context *ctx = heif_context_alloc();
+
+ heif_encoder *encoder;
+ heif_error err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder);
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_encoding_options *options = heif_encoding_options_alloc();
+ options->macOS_compatibility_workaround_no_nclx_profile = true;
+
+ heif_image_handle *tiled_image;
+ err = heif_context_add_empty_unci_image(ctx, ¶ms, options, input_image, &tiled_image);
+ REQUIRE(err.code == heif_error_Ok);
+
+ // Add each 16x16 tile carrying the corresponding sub-region of the input.
+ for (uint32_t ty = 0; ty < 2; ty++) {
+ for (uint32_t tx = 0; tx < 2; tx++) {
+ heif_image *tile;
+ err = heif_image_create(TW, TH, heif_colorspace_YCbCr, heif_chroma_420, &tile);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_image_add_plane(tile, heif_channel_Y, TW, TH, 8);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_image_add_plane(tile, heif_channel_Cb, TW / 2, TH / 2, 8);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_image_add_plane(tile, heif_channel_Cr, TW / 2, TH / 2, 8);
+ REQUIRE(err.code == heif_error_Ok);
+
+ int y_stride, cb_stride, cr_stride;
+ uint8_t *tY = heif_image_get_plane(tile, heif_channel_Y, &y_stride);
+ uint8_t *tCb = heif_image_get_plane(tile, heif_channel_Cb, &cb_stride);
+ uint8_t *tCr = heif_image_get_plane(tile, heif_channel_Cr, &cr_stride);
+
+ int src_y_stride, src_cb_stride, src_cr_stride;
+ const uint8_t *sY = heif_image_get_plane(input_image, heif_channel_Y, &src_y_stride);
+ const uint8_t *sCb = heif_image_get_plane(input_image, heif_channel_Cb, &src_cb_stride);
+ const uint8_t *sCr = heif_image_get_plane(input_image, heif_channel_Cr, &src_cr_stride);
+
+ for (int row = 0; row < TH; row++) {
+ memcpy(tY + row * y_stride,
+ sY + (ty * TH + row) * src_y_stride + tx * TW,
+ TW);
+ }
+ for (int row = 0; row < TH / 2; row++) {
+ memcpy(tCb + row * cb_stride,
+ sCb + (ty * (TH / 2) + row) * src_cb_stride + tx * (TW / 2),
+ TW / 2);
+ memcpy(tCr + row * cr_stride,
+ sCr + (ty * (TH / 2) + row) * src_cr_stride + tx * (TW / 2),
+ TW / 2);
+ }
+
+ err = heif_context_add_image_tile(ctx, tiled_image, tx, ty, tile, encoder);
+ REQUIRE(err.code == heif_error_Ok);
+ heif_image_release(tile);
+ }
+ }
+
+ err = heif_context_write_to_file(ctx, "encode_ycbcr420_tiled.heif");
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_image_handle_release(tiled_image);
+
+ // --- decode and check chroma placement (the bug placed second-tile-row
+ // chroma far past the end of the chroma plane).
+
+ heif_context *decode_context = heif_context_alloc();
+ err = heif_context_read_from_file(decode_context, "encode_ycbcr420_tiled.heif", NULL);
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_image_handle *decode_handle = get_primary_image_handle(decode_context);
+ heif_image *decoded;
+ err = heif_decode_image(decode_handle, &decoded, heif_colorspace_YCbCr, heif_chroma_420, NULL);
+ REQUIRE(err.code == heif_error_Ok);
+
+ int stride;
+ const uint8_t *cb = heif_image_get_plane_readonly(decoded, heif_channel_Cb, &stride);
+ REQUIRE(cb != nullptr);
+ for (int row = 0; row < H / 2; row++) {
+ int expected_tile_row = (row < H / 4) ? 0 : 1;
+ for (int col = 0; col < W / 2; col++) {
+ int expected_tile_col = (col < W / 4) ? 0 : 1;
+ uint8_t expected = static_cast<uint8_t>(0x10 + expected_tile_row * 2 + expected_tile_col);
+ INFO("Cb row=" << row << " col=" << col);
+ REQUIRE(((int) cb[row * stride + col]) == expected);
+ }
+ }
+
+ const uint8_t *cr = heif_image_get_plane_readonly(decoded, heif_channel_Cr, &stride);
+ REQUIRE(cr != nullptr);
+ for (int row = 0; row < H / 2; row++) {
+ int expected_tile_row = (row < H / 4) ? 0 : 1;
+ for (int col = 0; col < W / 2; col++) {
+ int expected_tile_col = (col < W / 4) ? 0 : 1;
+ uint8_t expected = static_cast<uint8_t>(0xA0 + expected_tile_row * 2 + expected_tile_col);
+ INFO("Cr row=" << row << " col=" << col);
+ REQUIRE(((int) cr[row * stride + col]) == expected);
+ }
+ }
+
+ heif_image_release(decoded);
+ heif_image_handle_release(decode_handle);
+ heif_context_free(decode_context);
+
+ heif_encoding_options_free(options);
+ heif_encoder_release(encoder);
+ heif_image_release(input_image);
+ heif_context_free(ctx);
+}