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")
{