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);
+ }
+}