Commit 9e2c938d for libheif

commit 9e2c938d33586070ed0ef7cc85215286468c01bf
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Fri Oct 2 00:41:16 2026 +0200

    Validate HEVC SPS before decoding (GHSA-86ch-j429-fpr2)

diff --git a/libheif/codecs/hevc_boxes.cc b/libheif/codecs/hevc_boxes.cc
index f0e490ee..d963758e 100644
--- a/libheif/codecs/hevc_boxes.cc
+++ b/libheif/codecs/hevc_boxes.cc
@@ -690,6 +690,12 @@ Error parse_sps_for_hvcC_configuration(const uint8_t* sps, size_t size,
   reader.skip_bits(4);

   uint8_t nMaxSubLayersMinus1 = reader.get_bits8(3);
+  if (nMaxSubLayersMinus1 > 6) {
+    // sps_max_sub_layers_minus1 is in the range 0..6 (H.265 section 7.4.3.2.1).
+    return Error{heif_error_Invalid_input,
+                 heif_suberror_Invalid_parameter_value,
+                 "SPS sps_max_sub_layers_minus1 out of range"};
+  }

   config->temporal_id_nested = reader.get_bits8(1);

@@ -724,9 +730,13 @@ Error parse_sps_for_hvcC_configuration(const uint8_t* sps, size_t size,

   for (int i = 0; i < nMaxSubLayersMinus1; i++) {
     if (layer_profile_present[i]) {
+      // Same 88 bits as the general profile above: space, tier, idc,
+      // compatibility flags, then 48 bits of source and constraint flags.
       reader.skip_bits(2 + 1 + 5);
       reader.skip_bits(32);
       reader.skip_bits(16);
+      reader.skip_bits(16);
+      reader.skip_bits(16);
     }

     if (layer_level_present[i]) {
@@ -743,9 +753,17 @@ Error parse_sps_for_hvcC_configuration(const uint8_t* sps, size_t size,
     "Invalid variable length code in HEVC SPS header"
   };

-  uint32_t dummy, value;
-  if (!reader.get_uvlc(&dummy) || // skip seq_parameter_seq_id
-      !reader.get_uvlc(&value)) {
+  uint32_t value;
+  if (!reader.get_uvlc(&value)) {
+    return invalidUVLC;
+  }
+  if (value > 15) {
+    return Error{heif_error_Invalid_input,
+                 heif_suberror_Invalid_parameter_value,
+                 "SPS seq_parameter_set_id out of range"};
+  }
+
+  if (!reader.get_uvlc(&value)) {
     return invalidUVLC;
   }
   if (value > 3) {
@@ -765,6 +783,11 @@ Error parse_sps_for_hvcC_configuration(const uint8_t* sps, size_t size,
       !reader.get_uvlc(height)) {
     return invalidUVLC;
   }
+  if (*width == 0 || *height == 0 || *width > 65535 || *height > 65535) {
+    return Error{heif_error_Invalid_input,
+                 heif_suberror_Invalid_parameter_value,
+                 "SPS picture size out of range"};
+  }

   if (coded_size) {
     coded_size->width = *width;
@@ -792,7 +815,7 @@ Error parse_sps_for_hvcC_configuration(const uint8_t* sps, size_t size,

     const uint64_t crop_w = (uint64_t)subH * ((uint64_t)left + (uint64_t)right);
     const uint64_t crop_h = (uint64_t)subV * ((uint64_t)top + (uint64_t)bottom);
-    if (crop_w > *width || crop_h > *height) {
+    if (crop_w >= *width || crop_h >= *height) {
       return Error{heif_error_Invalid_input,
                    heif_suberror_Invalid_parameter_value,
                    "SPS conformance window exceeds image dimensions"};
@@ -821,6 +844,13 @@ Error parse_sps_for_hvcC_configuration(const uint8_t* sps, size_t size,
   }
   config->bit_depth_chroma = (uint8_t) (value + 8);

+  if (reader.get_bits_remaining() < 0) {
+    // The reader returns zeros past the end, so a truncated SPS would
+    // otherwise parse with made-up values.
+    return Error{heif_error_Invalid_input,
+                 heif_suberror_End_of_data,
+                 "SPS header is truncated"};
+  }


   // --- init static configuration fields ---
diff --git a/libheif/codecs/hevc_dec.cc b/libheif/codecs/hevc_dec.cc
index d7458b42..ca56bd98 100644
--- a/libheif/codecs/hevc_dec.cc
+++ b/libheif/codecs/hevc_dec.cc
@@ -82,9 +82,9 @@ Result<std::optional<ImageSize>> Decoder_HEVC::get_max_coded_image_size(const st
     Error e = parse_sps_for_hvcC_configuration(nal_data, nal_size, &scratch,
                                                &cropped_w, &cropped_h, &coded);
     if (e) {
-      // A malformed SPS we cannot parse is skipped rather than failing the whole
-      // decode; the decoder plugin applies its own limits when it reaches it.
-      continue;
+      // An SPS we cannot parse has an unknown coded size, so it must not reach the
+      // decoder plugin: not every plugin enforces the size limits before allocating.
+      return e;
     }

     found = true;
@@ -93,6 +93,8 @@ Result<std::optional<ImageSize>> Decoder_HEVC::get_max_coded_image_size(const st
   }

   if (!found) {
+    // Later samples of a sequence reuse the parameter sets of earlier ones, and
+    // those were already checked when they were pushed.
     return std::optional<ImageSize>{};
   }

diff --git a/libheif/plugins/decoder_ffmpeg.cc b/libheif/plugins/decoder_ffmpeg.cc
index 4d02823b..cc5bc580 100644
--- a/libheif/plugins/decoder_ffmpeg.cc
+++ b/libheif/plugins/decoder_ffmpeg.cc
@@ -31,6 +31,7 @@

 #include <deque>
 #include <iostream>
+#include <limits>
 #include <memory>
 #include <utility>
 #include <vector>
@@ -87,6 +88,20 @@ static bool supportsNal(AVCodecID id) {
   return id == AV_CODEC_ID_H264 || id == AV_CODEC_ID_H265 || id == AV_CODEC_ID_H266;
 }

+// Emulation prevention keeps 00 00 00, 00 00 01 and 00 00 02 out of every valid
+// NAL unit. The NAL units are handed to FFmpeg with start codes, so such a byte
+// sequence would make FFmpeg split the data differently than libheif did when it
+// checked the parameter sets.
+static bool contains_start_code_prefix(const uint8_t* data, size_t size)
+{
+  for (size_t i = 2; i < size; i++) {
+    if (data[i] <= 2 && data[i - 1] == 0 && data[i - 2] == 0) {
+      return true;
+    }
+  }
+  return false;
+}
+
 // Codecs whose bitstream carries CICP colour signalling (VUI / sequence header).
 static bool codec_has_cicp_signalling(AVCodecID id)
 {
@@ -230,6 +245,14 @@ static heif_error ffmpeg_new_decoder2(void** dec, const heif_decoder_plugin_opti
   // wider than the size signaled in the file.
   decoder->av_codec_context->flags |= AV_CODEC_FLAG_UNALIGNED;

+  // Let FFmpeg refuse pictures larger than libheif's limit for this image
+  // (ispe plus a coding-unit margin), as the libde265 plugin does.
+  const heif_security_limits* limits = options->limits ? options->limits : heif_get_global_security_limits();
+  if (limits->max_image_size_pixels > 0 &&
+      limits->max_image_size_pixels <= static_cast<uint64_t>(std::numeric_limits<int64_t>::max())) {
+    decoder->av_codec_context->max_pixels = static_cast<int64_t>(limits->max_image_size_pixels);
+  }
+
   /* open it */
   if (avcodec_open2(decoder->av_codec_context, decoder->av_codec, NULL) < 0) {
     return { heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "avcodec_open2 returned error" };
@@ -356,6 +379,14 @@ static heif_error ffmpeg_push_data2(void *decoder_raw, const void *data, size_t
         };
       }

+      if (contains_start_code_prefix(cdata + ptr, nal_size)) {
+        return {
+          heif_error_Decoder_plugin_error,
+          heif_suberror_Unspecified,
+          "NAL unit contains a start code prefix"
+        };
+      }
+
       pkt.data.push_back(0);
       pkt.data.push_back(0);
       pkt.data.push_back(1);
diff --git a/tests/hevc_sps.cc b/tests/hevc_sps.cc
index 20b6d74a..d94b1a70 100644
--- a/tests/hevc_sps.cc
+++ b/tests/hevc_sps.cc
@@ -26,6 +26,7 @@

 #include "catch_amalgamated.hpp"
 #include "codecs/hevc_boxes.h"
+#include "codecs/hevc_dec.h"
 #include "codecs/decoder.h"
 #include "bitstream.h"
 #include "error.h"
@@ -43,6 +44,99 @@ static const std::vector<uint8_t> rainbow_sps{
     0x96, 0xea, 0x49, 0x29, 0xae, 0x6e, 0x02, 0x1a, 0x0c, 0x08, 0x00, 0x00,
     0x03, 0x00, 0xc8, 0x00, 0x00, 0x03, 0x00, 0x08, 0x40};

+static void write_uvlc(BitWriter& writer, uint32_t value)
+{
+  uint32_t code = value + 1;
+  int bits = 0;
+  for (uint32_t n = code; n != 0; n >>= 1) {
+    bits++;
+  }
+  writer.write_bits(0, bits - 1);
+  writer.write_bits(code, bits);
+}
+
+
+static std::vector<uint8_t> sps_with_sub_layer_profile()
+{
+  BitWriter writer;
+  writer.write_bits8(0x42, 8); // SPS NAL header
+  writer.write_bits8(0x01, 8);
+  writer.write_bits(0, 4);     // sps_video_parameter_set_id
+  writer.write_bits(1, 3);     // sps_max_sub_layers_minus1
+  writer.write_flag(true);     // sps_temporal_id_nesting_flag
+
+  writer.write_bits(1, 8);     // general profile space, tier, and idc
+  writer.write_bits32(0, 32);  // general compatibility flags
+  writer.write_bits16(0, 16);
+  writer.write_bits16(0, 16);
+  writer.write_bits16(0, 16);  // 48 general source and constraint flags
+  writer.write_bits8(120, 8);  // general_level_idc
+
+  writer.write_flag(true);     // sub_layer_profile_present_flag[0]
+  writer.write_flag(true);     // sub_layer_level_present_flag[0]
+  for (int i = 1; i < 8; i++) {
+    writer.write_bits(0, 2);   // reserved_zero_2bits
+  }
+  writer.write_bits(1, 8);     // sub-layer profile space, tier, and idc
+  writer.write_bits32(0, 32);  // sub-layer compatibility flags
+  writer.write_bits16(0, 16);
+  writer.write_bits16(0, 16);
+  writer.write_bits16(0, 16);  // 48 sub-layer source and constraint flags
+  writer.write_bits8(120, 8);  // sub_layer_level_idc[0]
+
+  write_uvlc(writer, 0);       // sps_seq_parameter_set_id
+  write_uvlc(writer, 1);       // chroma_format_idc (4:2:0)
+  write_uvlc(writer, 64);      // pic_width_in_luma_samples
+  write_uvlc(writer, 48);      // pic_height_in_luma_samples
+  writer.write_flag(false);    // conformance_window_flag
+  write_uvlc(writer, 0);       // bit_depth_luma_minus8
+  write_uvlc(writer, 0);       // bit_depth_chroma_minus8
+  return writer.get_data();
+}
+
+
+TEST_CASE("SPS sub-layer profile consumes all 88 bits")
+{
+  std::vector<uint8_t> sps = sps_with_sub_layer_profile();
+  HEVCDecoderConfigurationRecord config{};
+  uint32_t width = 0, height = 0;
+  ImageSize coded{};
+
+  Error err = parse_sps_for_hvcC_configuration(sps.data(), sps.size(),
+                                               &config, &width, &height, &coded);
+  REQUIRE(!err);
+  CHECK(width == 64);
+  CHECK(height == 48);
+  CHECK(coded.width == 64);
+  CHECK(coded.height == 48);
+}
+
+
+TEST_CASE("HEVC coded-size scan rejects a malformed SPS after a valid one")
+{
+  std::vector<uint8_t> invalid_sps = rainbow_sps;
+  REQUIRE((invalid_sps[18] & 0xF0) == 0xA0);
+  invalid_sps[18] = 0x94 | (invalid_sps[18] & 0x03); // chroma_format_idc = 4
+
+  std::vector<uint8_t> data;
+  auto append_nal = [&data](const std::vector<uint8_t>& nal) {
+    uint32_t size = static_cast<uint32_t>(nal.size());
+    data.push_back(static_cast<uint8_t>(size >> 24));
+    data.push_back(static_cast<uint8_t>(size >> 16));
+    data.push_back(static_cast<uint8_t>(size >> 8));
+    data.push_back(static_cast<uint8_t>(size));
+    data.insert(data.end(), nal.begin(), nal.end());
+  };
+  append_nal(rainbow_sps);
+  append_nal(invalid_sps);
+
+  auto box = std::make_shared<Box_hvcC>();
+  Decoder_HEVC decoder(box);
+  auto result = decoder.get_max_coded_image_size(data);
+  REQUIRE(result.is_error());
+  CHECK(result.error().error_code == heif_error_Invalid_input);
+}
+

 TEST_CASE("SPS conformance window yields visible and coded size")
 {