Commit 2a407685 for libheif

commit 2a407685a835186f3028119e2e4a4c8e30356bf6
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Sat Oct 24 22:24:15 2026 +0200

    Reject unparseable AVC and VVC SPS NAL units [GHSA-xwcv-cp39-m2w4]

diff --git a/libheif/codecs/avc_dec.cc b/libheif/codecs/avc_dec.cc
index 1a51207f..3fe1ac75 100644
--- a/libheif/codecs/avc_dec.cc
+++ b/libheif/codecs/avc_dec.cc
@@ -69,7 +69,10 @@ Result<std::optional<ImageSize>> Decoder_AVC::get_max_coded_image_size(const std
       continue;
     }
     int nal_type = nal_data[0] & 0x1F;
-    if (nal_type != AVC_NAL_UNIT_SPS_NUT) {
+    // A subset SPS (SVC, MVC) starts with the same seq_parameter_set_data() as
+    // an SPS and can replace the parameters used by the decoder.
+    if (nal_type != AVC_NAL_UNIT_SPS_NUT &&
+        nal_type != AVC_NAL_UNIT_SUBSET_SPS_NUT) {
       continue;
     }

@@ -79,9 +82,9 @@ Result<std::optional<ImageSize>> Decoder_AVC::get_max_coded_image_size(const std
     Error e = parse_sps_for_avcC_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 and unchecked cropping
+      // offsets, so it must not reach the decoder plugin.
+      return e;
     }

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

   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/codecs/vvc_dec.cc b/libheif/codecs/vvc_dec.cc
index 4741b6df..8e0d3e6c 100644
--- a/libheif/codecs/vvc_dec.cc
+++ b/libheif/codecs/vvc_dec.cc
@@ -89,9 +89,9 @@ Result<std::optional<ImageSize>> Decoder_VVC::get_max_coded_image_size(const std
     Error e = parse_sps_for_vvcC_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.
+      return e;
     }

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

   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/nalu_utils.h b/libheif/plugins/nalu_utils.h
index d6500319..eec4ac9a 100644
--- a/libheif/plugins/nalu_utils.h
+++ b/libheif/plugins/nalu_utils.h
@@ -26,6 +26,7 @@ static const int AVC_NAL_UNIT_MAX_VCL    = 5;
 static const int AVC_NAL_UNIT_SPS_NUT    = 7;
 static const int AVC_NAL_UNIT_PPS_NUT    = 8;
 static const int AVC_NAL_UNIT_SPS_EXT_NUT = 13;
+static const int AVC_NAL_UNIT_SUBSET_SPS_NUT = 15;

 static const int HEVC_NAL_UNIT_MAX_VCL    = 31;
 static const int HEVC_NAL_UNIT_VPS_NUT    = 32;
@@ -71,4 +72,4 @@ public:
     void clear();
 private:
     std::map<int, std::unique_ptr<NalUnit>> map;
-};
\ No newline at end of file
+};
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 1513b6ac..3675689a 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -71,6 +71,7 @@ add_libheif_test(sdtp_sample_limit)
     add_libheif_test(add_channel_checks)
     add_libheif_test(jpeg2000)
     add_libheif_test(avc_box)
+    add_libheif_test(vvc_sps)
     add_libheif_test(av1_coded_size)
     add_libheif_test(hevc_sps)
     add_libheif_test(hevc_brands)
diff --git a/tests/avc_box.cc b/tests/avc_box.cc
index 9d3191d7..a672059d 100644
--- a/tests/avc_box.cc
+++ b/tests/avc_box.cc
@@ -26,6 +26,7 @@

 #include "catch_amalgamated.hpp"
 #include "codecs/avc_boxes.h"
+#include "codecs/avc_dec.h"
 #include "codecs/decoder.h"
 #include "error.h"
 #include <cstdint>
@@ -186,3 +187,45 @@ TEST_CASE("AVC SPS conformance window crop units") {
     REQUIRE(height == 152);
   }
 }
+
+
+TEST_CASE("AVC coded-size scan rejects an unparseable SPS")
+{
+  // Baseline profile SPS for a 64x64 picture without cropping.
+  const std::vector<uint8_t> valid_sps{
+      0x67, 0x42, 0xc0, 0x0a, 0xda, 0x10, 0x9c, 0x9d};
+
+  // The same SPS with frame_crop_top_offset = 2^14+1 and
+  // frame_crop_bottom_offset = 2^31-2^14-1. The latter has a 30-zero
+  // Exp-Golomb prefix that libheif rejects. It must not reach the decoder.
+  const std::vector<uint8_t> invalid_sps{
+      0x67, 0x42, 0xc0, 0x0a, 0xda, 0x10, 0x9c, 0x90, 0x00, 0x20,
+      0x01, 0x00, 0x00, 0x03, 0x00, 0x01, 0xff, 0xff, 0x00, 0x01};
+
+  std::vector<uint8_t> data;
+  auto append_nal = [&data](std::vector<uint8_t> nal, uint8_t nal_header) {
+    nal[0] = nal_header;
+    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());
+  };
+
+  Decoder_AVC decoder(std::make_shared<Box_avcC>());
+
+  SECTION("ordinary SPS") {
+    append_nal(valid_sps, 0x67);
+    append_nal(invalid_sps, 0x67);
+  }
+
+  SECTION("subset SPS") {
+    append_nal(valid_sps, 0x67);
+    append_nal(invalid_sps, 0x6f);
+  }
+
+  auto result = decoder.get_max_coded_image_size(data);
+  REQUIRE(result.is_error());
+  CHECK(result.error().error_code == heif_error_Invalid_input);
+}
diff --git a/tests/vvc_sps.cc b/tests/vvc_sps.cc
new file mode 100644
index 00000000..3ef20a1e
--- /dev/null
+++ b/tests/vvc_sps.cc
@@ -0,0 +1,114 @@
+/*
+  libheif VVC SPS parsing unit tests
+
+  MIT License
+
+  Copyright (c) 2026 Dirk Farin <dirk.farin@gmail.com>
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+*/
+
+#include "catch_amalgamated.hpp"
+#include "bitstream.h"
+#include "codecs/vvc_boxes.h"
+#include "codecs/vvc_dec.h"
+#include "libheif/heif.h"
+
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+
+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> make_vvc_sps(bool valid)
+{
+  BitWriter writer;
+  writer.write_bits8(0, 8);     // forbidden bit, reserved bit, layer ID
+  writer.write_bits8(0x79, 8);  // SPS NAL type 15, temporal_id_plus1 = 1
+  writer.write_bits(0, 4);      // sps_seq_parameter_set_id
+  writer.write_bits(0, 4);      // sps_video_parameter_set_id
+  writer.write_bits(0, 3);      // sps_max_sublayers_minus1
+  writer.write_bits(1, 2);      // sps_chroma_format_idc = 4:2:0
+  writer.write_bits(0, 2);      // sps_log2_ctu_size_minus5
+  writer.write_flag(false);     // sps_ptl_dpb_hrd_params_present_flag
+  writer.write_flag(false);     // sps_gdr_enabled_flag
+  writer.write_flag(false);     // sps_ref_pic_resampling_enabled_flag
+
+  if (!valid) {
+    // BitReader::get_uvlc() rejects the width after 21 leading zeros.
+    writer.write_bits(0, 21);
+    writer.write_flag(true);
+    return writer.get_data();
+  }
+
+  write_uvlc(writer, 64);       // sps_pic_width_max_in_luma_samples
+  write_uvlc(writer, 48);       // sps_pic_height_max_in_luma_samples
+  writer.write_flag(false);     // sps_conformance_window_flag
+  writer.write_flag(false);     // sps_subpic_info_present_flag
+  write_uvlc(writer, 0);        // sps_bitdepth_minus8
+  return writer.get_data();
+}
+
+
+static void append_nal(std::vector<uint8_t>& 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());
+}
+
+
+TEST_CASE("VVC coded-size scan rejects an unparseable SPS")
+{
+  std::vector<uint8_t> data;
+  Decoder_VVC decoder(std::make_shared<Box_vvcC>());
+
+  SECTION("valid SPS") {
+    append_nal(data, make_vvc_sps(true));
+
+    auto result = decoder.get_max_coded_image_size(data);
+    REQUIRE(!result.is_error());
+    REQUIRE(result->has_value());
+    CHECK((*result)->width == 64);
+    CHECK((*result)->height == 48);
+  }
+
+  SECTION("unparseable SPS after a valid one") {
+    append_nal(data, make_vvc_sps(true));
+    append_nal(data, make_vvc_sps(false));
+
+    auto result = decoder.get_max_coded_image_size(data);
+    REQUIRE(result.is_error());
+    CHECK(result.error().error_code == heif_error_Invalid_input);
+  }
+}