Commit e8e84bb0 for libheif
commit e8e84bb02e337b79c7a4dc3b84872978965936b6
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Sat Oct 24 22:11:45 2026 +0200
Parse full-length AV1 UVLC values [GHSA-fpr5-xpfr-vcq3]
diff --git a/libheif/codecs/avif_boxes.cc b/libheif/codecs/avif_boxes.cc
index 3e4a241c..48bb176a 100644
--- a/libheif/codecs/avif_boxes.cc
+++ b/libheif/codecs/avif_boxes.cc
@@ -289,6 +289,42 @@ static bool leb128(BitReader& reader, uint64_t* out_value)
}
+// uvlc() as defined by the AV1 specification. AV1 permits up to 31 leading
+// zero bits. BitReader::get_uvlc() is shared with other codecs and deliberately
+// stops after 20, so using it here would leave the reader in the middle of a
+// valid AV1 code and desynchronize all following fields.
+static bool av1_uvlc(BitReader& reader, uint32_t* out_value)
+{
+ int leading_zeros = 0;
+ while (true) {
+ if (reader.get_bits_remaining() < 1) {
+ return false;
+ }
+
+ if (reader.get_bits(1) != 0) {
+ break;
+ }
+
+ leading_zeros++;
+ if (leading_zeros >= 32) {
+ return false;
+ }
+ }
+
+ if (reader.get_bits_remaining() < leading_zeros) {
+ return false;
+ }
+
+ uint32_t value = (uint32_t{1} << leading_zeros) - 1;
+ if (leading_zeros != 0) {
+ value += reader.get_bits(leading_zeros);
+ }
+
+ *out_value = value;
+ return true;
+}
+
+
struct obu_header_info
{
int type = 0;
@@ -399,7 +435,9 @@ bool fill_av1C_configuration_from_stream(Box_av1C::configuration* out_config, co
reader.skip_bytes(2 * 4);
bool equal_picture_interval = reader.get_bits(1);
if (equal_picture_interval) {
- reader.get_uvlc(&dummy);
+ if (!av1_uvlc(reader, &dummy)) {
+ return false;
+ }
}
// --- skip decoder_model_info
@@ -612,7 +650,9 @@ static bool parse_av1_seq_header_max_frame_size(BitReader& reader,
reader.skip_bytes(2 * 4);
bool equal_picture_interval = reader.get_bits(1);
if (equal_picture_interval) {
- reader.get_uvlc(&dummy);
+ if (!av1_uvlc(reader, &dummy)) {
+ return false;
+ }
}
// --- skip decoder_model_info
diff --git a/tests/av1_coded_size.cc b/tests/av1_coded_size.cc
index 15013dbd..c167f12a 100644
--- a/tests/av1_coded_size.cc
+++ b/tests/av1_coded_size.cc
@@ -52,6 +52,52 @@ static void append_sequence_header(std::vector<uint8_t>& data, uint32_t width, u
}
+static void append_sequence_header_with_uvlc(std::vector<uint8_t>& data,
+ uint32_t width, uint32_t height,
+ int leading_zeros)
+{
+ BitWriter writer;
+ writer.write_bits(0, 3); // seq_profile
+ writer.write_bits(0, 1); // still_picture
+ writer.write_bits(0, 1); // reduced_still_picture_header
+ writer.write_bits(1, 1); // timing_info_present_flag
+ writer.write_bits(1, 32); // num_units_in_display_tick
+ writer.write_bits(1, 32); // time_scale
+ writer.write_bits(1, 1); // equal_picture_interval
+
+ for (int i = 0; i < leading_zeros; i++) {
+ writer.write_bits(0, 1);
+ }
+ writer.write_bits(1, 1);
+ if (leading_zeros != 0) {
+ writer.write_bits(0, leading_zeros); // uvlc value bits
+ }
+
+ writer.write_bits(0, 1); // decoder_model_info_present_flag
+ writer.write_bits(0, 1); // initial_display_delay_present_flag
+ writer.write_bits(0, 5); // operating_points_cnt_minus_1
+ writer.write_bits(0, 12); // operating_point_idc[0]
+ writer.write_bits(16, 5); // seq_level_idx[0]
+ writer.write_bits(0, 1); // seq_tier[0]
+ writer.write_bits(15, 4); // frame_width_bits_minus_1
+ writer.write_bits(15, 4); // frame_height_bits_minus_1
+ writer.write_bits(width - 1, 16);
+ writer.write_bits(height - 1, 16);
+
+ // Remaining sequence-header fields for an 8-bit 4:2:0 stream, followed by
+ // the trailing_one_bit. These are not needed by the coded-size scanner, but
+ // make the same OBU usable by fill_av1C_configuration_from_stream().
+ writer.write_bits(0b000100101011111001000000000, 27);
+ writer.write_bits(1, 1);
+
+ std::vector<uint8_t> payload = writer.get_data();
+ REQUIRE(payload.size() < 128);
+ data.push_back(0x0a); // obu_type = 1, obu_has_size_field = 1
+ data.push_back(static_cast<uint8_t>(payload.size()));
+ data.insert(data.end(), payload.begin(), payload.end());
+}
+
+
TEST_CASE("largest of several AV1 sequence headers is found")
{
std::vector<uint8_t> data;
@@ -67,6 +113,39 @@ TEST_CASE("largest of several AV1 sequence headers is found")
}
+TEST_CASE("AV1 sequence-header UVLC permits up to 31 leading zeros")
+{
+ int leading_zeros = GENERATE(21, 31);
+ std::vector<uint8_t> data;
+ append_sequence_header_with_uvlc(data, 8192, 4096, leading_zeros);
+
+ uint32_t width = 0, height = 0;
+ auto result = find_max_av1_frame_size_in_stream(data.data(), data.size(), &width, &height);
+ REQUIRE(result);
+ REQUIRE(*result);
+ CHECK(width == 8192);
+ CHECK(height == 4096);
+
+ Box_av1C::configuration config;
+ CHECK(fill_av1C_configuration_from_stream(&config, data.data(), static_cast<int>(data.size())));
+}
+
+
+TEST_CASE("AV1 sequence-header UVLC rejects 32 leading zeros")
+{
+ std::vector<uint8_t> data;
+ append_sequence_header_with_uvlc(data, 8192, 4096, 32);
+
+ uint32_t width = 0, height = 0;
+ auto result = find_max_av1_frame_size_in_stream(data.data(), data.size(), &width, &height);
+ REQUIRE(result.is_error());
+ CHECK(result.error().message.find("sequence header cannot be scanned completely") != std::string::npos);
+
+ Box_av1C::configuration config;
+ CHECK_FALSE(fill_av1C_configuration_from_stream(&config, data.data(), static_cast<int>(data.size())));
+}
+
+
TEST_CASE("zero-sized AV1 OBU completes the scan")
{
std::vector<uint8_t> data;