Commit f91283ac for libheif

commit f91283aca524c33c349132fcde615563f245cfe9
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Wed Apr 29 19:33:46 2026 +0200

    H.264, H.265, H.266: check SPS image size in configuration boxes against libheif security limits (#1774)

diff --git a/libheif/bitstream.cc b/libheif/bitstream.cc
index f7b6a630..be29548e 100644
--- a/libheif/bitstream.cc
+++ b/libheif/bitstream.cc
@@ -696,7 +696,7 @@ void BitReader::skip_to_byte_boundary()
   nextbits_cnt -= nskip;
 }

-bool BitReader::get_uvlc(int* value)
+bool BitReader::get_uvlc(uint32_t* value)
 {
   int num_zeros = 0;

@@ -706,10 +706,9 @@ bool BitReader::get_uvlc(int* value)
     if (num_zeros > MAX_UVLC_LEADING_ZEROS) { return false; }
   }

-  int offset = 0;
   if (num_zeros != 0) {
-    offset = (int) get_bits(num_zeros);
-    *value = offset + (1 << num_zeros) - 1;
+    uint32_t offset = get_bits(num_zeros);
+    *value = offset + (1u << num_zeros) - 1u;
     assert(*value > 0);
     return true;
   }
@@ -719,19 +718,19 @@ bool BitReader::get_uvlc(int* value)
   }
 }

-bool BitReader::get_svlc(int* value)
+bool BitReader::get_svlc(int32_t* value)
 {
-  int v;
+  uint32_t v;
   if (!get_uvlc(&v)) {
     return false;
   }
   else if (v == 0) {
-    *value = v;
+    *value = 0;
     return true;
   }

-  bool negative = ((v & 1) == 0);
-  *value = negative ? -v / 2 : (v + 1) / 2;
+  bool negative = ((v & 1u) == 0);
+  *value = negative ? -static_cast<int32_t>(v / 2) : static_cast<int32_t>((v + 1) / 2);
   return true;
 }

diff --git a/libheif/bitstream.h b/libheif/bitstream.h
index a532ad74..9cd3928f 100644
--- a/libheif/bitstream.h
+++ b/libheif/bitstream.h
@@ -443,9 +443,9 @@ public:

   void skip_to_byte_boundary();

-  bool get_uvlc(int* value);
+  bool get_uvlc(uint32_t* value);

-  bool get_svlc(int* value);
+  bool get_svlc(int32_t* value);

   int get_current_byte_index() const
   {
diff --git a/libheif/codecs/avc_boxes.cc b/libheif/codecs/avc_boxes.cc
index ddd945f2..16033fb6 100644
--- a/libheif/codecs/avc_boxes.cc
+++ b/libheif/codecs/avc_boxes.cc
@@ -324,7 +324,7 @@ void skip_scaling_list(BitReader& reader, int sizeOfScalingList)
 #else
   // fast version
   for (int j = 0; j < sizeOfScalingList; j++) {
-    int delta_scale;
+    int32_t delta_scale;
     reader.get_svlc(&delta_scale);
     nextScale = (lastScale + delta_scale + 256) % 256;

@@ -340,7 +340,8 @@ void skip_scaling_list(BitReader& reader, int sizeOfScalingList)

 Error parse_sps_for_avcC_configuration(const uint8_t* sps, size_t size,
                                        Box_avcC::configuration* config,
-                                       int* width, int* height)
+                                       uint32_t* width, uint32_t* height,
+                                       ImageSize* coded_size)
 {
   // remove start-code emulation bytes from SPS header stream

@@ -361,7 +362,7 @@ Error parse_sps_for_avcC_configuration(const uint8_t* sps, size_t size,
   config->AVCLevelIndication = reader.get_bits8(8);
   config->lengthSize = 4;

-  int value;
+  uint32_t value;
   reader.get_uvlc(&value); // SPS ID

   Error invalidUVLC{
@@ -413,18 +414,19 @@ Error parse_sps_for_avcC_configuration(const uint8_t* sps, size_t size,
   }

   reader.get_uvlc(&value); // log2_max_frame_num_minus4
-  int pic_order_cnt_type;
+  uint32_t pic_order_cnt_type;
   reader.get_uvlc(&pic_order_cnt_type);
   if (pic_order_cnt_type == 0) {
     reader.get_uvlc(&value);
   }
   else if (pic_order_cnt_type == 1) {
     reader.get_bits(1);
-    reader.get_svlc(&value);
-    reader.get_svlc(&value);
-    int num_ref_franes_in_pic_order_cnt_cycle;
+    int32_t svalue;
+    reader.get_svlc(&svalue);
+    reader.get_svlc(&svalue);
+    uint32_t num_ref_franes_in_pic_order_cnt_cycle;
     reader.get_uvlc(&num_ref_franes_in_pic_order_cnt_cycle);
-    for (int i = 0; i < num_ref_franes_in_pic_order_cnt_cycle; i++) {
+    for (uint32_t i = 0; i < num_ref_franes_in_pic_order_cnt_cycle; i++) {
       reader.get_uvlc(&value);
     }
   }
@@ -432,14 +434,19 @@ Error parse_sps_for_avcC_configuration(const uint8_t* sps, size_t size,
   reader.get_uvlc(&value); // num_ref_frames
   reader.skip_bits(1);

-  int pic_width_in_mbs_minus1;
-  int pic_height_in_mbs_minus1;
+  uint32_t pic_width_in_mbs_minus1;
+  uint32_t pic_height_in_mbs_minus1;
   reader.get_uvlc(&pic_width_in_mbs_minus1);
   reader.get_uvlc(&pic_height_in_mbs_minus1);

   *width = (pic_width_in_mbs_minus1 + 1) * 16;
   *height = (pic_height_in_mbs_minus1 + 1) * 16;

+  if (coded_size) {
+    coded_size->width = *width;
+    coded_size->height = *height;
+  }
+
   uint32_t frame_mbs_only_flag = reader.get_bits(1);
   if (!frame_mbs_only_flag) {
     reader.skip_bits(1);
@@ -447,7 +454,7 @@ Error parse_sps_for_avcC_configuration(const uint8_t* sps, size_t size,
   reader.skip_bits(1);
   uint32_t frame_cropping_flag = reader.get_bits(1);
   if (frame_cropping_flag) {
-    int left, right, top, bottom;
+    uint32_t left, right, top, bottom;
     reader.get_uvlc(&left);
     reader.get_uvlc(&right);
     reader.get_uvlc(&top);
diff --git a/libheif/codecs/avc_boxes.h b/libheif/codecs/avc_boxes.h
index 22c60177..6d043604 100644
--- a/libheif/codecs/avc_boxes.h
+++ b/libheif/codecs/avc_boxes.h
@@ -113,8 +113,14 @@ public:
   }
 };

+struct ImageSize;
+
+// Parses an AVC/H.264 SPS NAL unit. *width / *height return the post-frame-
+// cropping (display) dimensions. If non-null, *coded_size receives the
+// pre-cropping dimensions actually allocated by the decoder.
 Error parse_sps_for_avcC_configuration(const uint8_t* sps, size_t size,
                                        Box_avcC::configuration* inout_config,
-                                       int* width, int* height);
+                                       uint32_t* width, uint32_t* height,
+                                       ImageSize* coded_size = nullptr);

 #endif
diff --git a/libheif/codecs/avc_dec.cc b/libheif/codecs/avc_dec.cc
index 8af79a4c..3a0a2ac7 100644
--- a/libheif/codecs/avc_dec.cc
+++ b/libheif/codecs/avc_dec.cc
@@ -47,6 +47,29 @@ int Decoder_AVC::get_chroma_bits_per_pixel() const
 }


+Result<std::optional<ImageSize>> Decoder_AVC::get_coded_image_size_from_config() const
+{
+  const auto& sps_set = m_avcC->getSequenceParameterSets();
+
+  for (const auto& sps : sps_set) {
+    if (sps.empty()) continue;
+    Box_avcC::configuration scratch = m_avcC->get_configuration();
+    uint32_t cropped_w = 0, cropped_h = 0;
+    ImageSize coded{};
+
+    Error e = parse_sps_for_avcC_configuration(sps.data(), sps.size(), &scratch,
+                                               &cropped_w, &cropped_h, &coded);
+    if (e) {
+      return e;
+    }
+
+    return std::optional<ImageSize>{coded};
+  }
+
+  return std::optional<ImageSize>{};
+}
+
+
 Error Decoder_AVC::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const
 {
   *out_chroma = m_avcC->get_configuration().chroma_format;
diff --git a/libheif/codecs/avc_dec.h b/libheif/codecs/avc_dec.h
index 3d08653b..5613e533 100644
--- a/libheif/codecs/avc_dec.h
+++ b/libheif/codecs/avc_dec.h
@@ -46,6 +46,8 @@ public:

   Result<std::vector<uint8_t>> read_bitstream_configuration_data() const override;

+  Result<std::optional<ImageSize>> get_coded_image_size_from_config() const override;
+
 private:
   const std::shared_ptr<const Box_avcC> m_avcC;
 };
diff --git a/libheif/codecs/avc_enc.cc b/libheif/codecs/avc_enc.cc
index 1e6973fe..e9fed1a3 100644
--- a/libheif/codecs/avc_enc.cc
+++ b/libheif/codecs/avc_enc.cc
@@ -49,8 +49,8 @@ Result<Encoder::CodedImageData> Encoder_AVC::encode(const std::shared_ptr<HeifPi
                  err.message);
   }

-  int encoded_width = 0;
-  int encoded_height = 0;
+  uint32_t encoded_width = 0;
+  uint32_t encoded_height = 0;

   for (;;) {
     uint8_t* data;
@@ -109,8 +109,8 @@ Result<Encoder::CodedImageData> Encoder_AVC::encode(const std::shared_ptr<HeifPi
                                         &check_encoded_width,
                                         &check_encoded_height);

-    assert((int)check_encoded_width == encoded_width);
-    assert((int)check_encoded_height == encoded_height);
+    assert(check_encoded_width == encoded_width);
+    assert(check_encoded_height == encoded_height);
   }

   codedImage.codingConstraints.intra_pred_used = true;
diff --git a/libheif/codecs/avc_enc.h b/libheif/codecs/avc_enc.h
index c63d406d..c879be8e 100644
--- a/libheif/codecs/avc_enc.h
+++ b/libheif/codecs/avc_enc.h
@@ -65,8 +65,8 @@ private:
   std::shared_ptr<class Box_avcC> m_avcC;
   bool m_avcC_sent = false;

-  int m_encoded_image_width = 0;
-  int m_encoded_image_height = 0;
+  uint32_t m_encoded_image_width = 0;
+  uint32_t m_encoded_image_height = 0;

   std::optional<CodedImageData> m_current_output_data;
   bool m_output_image_complete = false;
diff --git a/libheif/codecs/avif_boxes.cc b/libheif/codecs/avif_boxes.cc
index 3a79d791..db3e75d4 100644
--- a/libheif/codecs/avif_boxes.cc
+++ b/libheif/codecs/avif_boxes.cc
@@ -361,7 +361,7 @@ bool fill_av1C_configuration_from_stream(Box_av1C::configuration* out_config, co

   // --- read sequence header

-  int dummy; // throw away value
+  uint32_t dummy; // throw away value

   bool decoder_model_info_present = false;
   int buffer_delay_length_minus1 = 0;
diff --git a/libheif/codecs/decoder.cc b/libheif/codecs/decoder.cc
index 33690fec..5176e732 100644
--- a/libheif/codecs/decoder.cc
+++ b/libheif/codecs/decoder.cc
@@ -25,6 +25,7 @@
 #include "context.h"
 #include "plugin_registry.h"
 #include "api_structs.h"
+#include "security_limits.h"

 #include "codecs/hevc_dec.h"
 #include "codecs/avif_dec.h"
@@ -322,6 +323,25 @@ Error Decoder::decode_sequence_frame_from_compressed_data(bool upload_configurat
     return pluginErr;
   }

+  // Reject memory-bomb inputs whose codec configuration record (SPS) declares
+  // a coded picture size beyond libheif's security limits, before handing any
+  // bytes to the decoder plugin. Codecs whose configuration record does not
+  // carry dimensions (e.g. AV1's av1C) return nullopt and skip the check.
+  //
+  // TODO: check this also in the decoder plugin since SPS packets may be
+  //       found within the actual image bitstream.
+  auto codedSize = get_coded_image_size_from_config();
+  if (codedSize.is_error()) {
+    return codedSize.error();
+  }
+
+  if (codedSize->has_value()) {
+    Error sizeErr = check_for_valid_image_size(limits, (*codedSize)->width, (*codedSize)->height);
+    if (sizeErr) {
+      return sizeErr;
+    }
+  }
+
   // --- decode image with the plugin

   heif_error err;
diff --git a/libheif/codecs/decoder.h b/libheif/codecs/decoder.h
index e76958b8..16dbf25a 100644
--- a/libheif/codecs/decoder.h
+++ b/libheif/codecs/decoder.h
@@ -27,12 +27,23 @@
 #include "file.h"

 #include <memory>
+#include <optional>
 #include <string>
 #include <utility>
 #include <vector>
 #include "image-items/hevc.h"


+// Image dimensions in luma samples. Reused wherever libheif needs to pass
+// a (width, height) pair around — initially for SPS-derived coded sizes,
+// but designed to fit other uses (ispe, image-item dimensions) over time.
+struct ImageSize
+{
+  uint32_t width;
+  uint32_t height;
+};
+
+
 // Specifies the input data for decoding.
 // For images, this points to the iloc extents.
 // For sequences, this points to the track data.
@@ -90,6 +101,23 @@ public:
   // Returns a stream of packets. Each packet is starts with a 4-byte size (MSB first).
   [[nodiscard]] virtual Result<std::vector<uint8_t>> read_bitstream_configuration_data() const = 0;

+  // Returns the *coded* picture size from the codec configuration record (the
+  // SPS for HEVC/AVC/VVC) — i.e. the buffer dimensions the decoder will
+  // actually allocate, BEFORE conformance-window cropping. The cropped output
+  // size is unsuitable for security checks: a malicious file can declare a
+  // huge SPS picture size with a near-equal-sized conformance window, so the
+  // displayed image looks small while the decoder still allocates the full
+  // uncropped buffer.
+  //
+  // Returns nullopt when the codec does not store dimensions in its
+  // configuration record (e.g. AV1's av1C) or when no SPS NAL is present.
+  // Returns Error only on a structurally invalid configuration record.
+  [[nodiscard]] virtual Result<std::optional<ImageSize>>
+  get_coded_image_size_from_config() const
+  {
+    return std::optional<ImageSize>{};
+  }
+
   Result<std::vector<uint8_t>> get_compressed_data(bool with_configuration_NALs) const;

   // --- decoding
diff --git a/libheif/codecs/hevc_boxes.cc b/libheif/codecs/hevc_boxes.cc
index 4c7a3108..c00237f0 100644
--- a/libheif/codecs/hevc_boxes.cc
+++ b/libheif/codecs/hevc_boxes.cc
@@ -451,12 +451,12 @@ static Result<std::shared_ptr<SEIMessage>> read_depth_representation_info(BitRea
   msg->has_d_min = (uint8_t) reader.get_bits(1);
   msg->has_d_max = (uint8_t) reader.get_bits(1);

-  int rep_type;
+  uint32_t rep_type;
   if (!reader.get_uvlc(&rep_type)) {
     return Error{heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "invalid depth representation type in input"};
   }

-  if (rep_type < 0 || rep_type > 3) {
+  if (rep_type > 3) {
     return Error{heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "input depth representation type out of range"};
   }

@@ -466,7 +466,7 @@ static Result<std::shared_ptr<SEIMessage>> read_depth_representation_info(BitRea
   //printf("type: %d\n",rep_type);

   if (msg->has_d_min || msg->has_d_max) {
-    int ref_view;
+    uint32_t ref_view;
     if (!reader.get_uvlc(&ref_view)) {
       return Error{heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "invalid disparity_reference_view in input"};
     }
@@ -593,7 +593,8 @@ std::vector<uint8_t> remove_start_code_emulation(const uint8_t* sps, size_t size

 Error parse_sps_for_hvcC_configuration(const uint8_t* sps, size_t size,
                                        HEVCDecoderConfigurationRecord* config,
-                                       int* width, int* height)
+                                       uint32_t* width, uint32_t* height,
+                                       ImageSize* coded_size)
 {
   // remove start-code emulation bytes from SPS header stream

@@ -657,7 +658,7 @@ Error parse_sps_for_hvcC_configuration(const uint8_t* sps, size_t size,

   // --- SPS continued ---

-  int dummy, value;
+  uint32_t dummy, value;
   reader.get_uvlc(&dummy); // skip seq_parameter_seq_id

   reader.get_uvlc(&value);
@@ -670,17 +671,22 @@ Error parse_sps_for_hvcC_configuration(const uint8_t* sps, size_t size,
   reader.get_uvlc(width);
   reader.get_uvlc(height);

+  if (coded_size) {
+    coded_size->width = *width;
+    coded_size->height = *height;
+  }
+
   bool conformance_window = reader.get_bits(1);
   if (conformance_window) {
-    int left, right, top, bottom;
+    uint32_t left, right, top, bottom;
     reader.get_uvlc(&left);
     reader.get_uvlc(&right);
     reader.get_uvlc(&top);
     reader.get_uvlc(&bottom);

-    //printf("conformance borders: %d %d %d %d\n",left,right,top,bottom);
+    //printf("conformance borders: %u %u %u %u\n",left,right,top,bottom);

-    int subH = 1, subV = 1;
+    uint32_t subH = 1, subV = 1;
     if (config->chroma_format == 1) {
       subV = 2;
       subH = 2;
diff --git a/libheif/codecs/hevc_boxes.h b/libheif/codecs/hevc_boxes.h
index 34e997ed..b77e30fb 100644
--- a/libheif/codecs/hevc_boxes.h
+++ b/libheif/codecs/hevc_boxes.h
@@ -162,8 +162,14 @@ Error decode_hevc_aux_sei_messages(const std::vector<uint8_t>& data,
 // Used for AVC, HEVC, and VVC.
 std::vector<uint8_t> remove_start_code_emulation(const uint8_t* sps, size_t size);

+struct ImageSize;
+
+// Parses an HEVC SPS NAL unit. *width / *height return the post-conformance-
+// window cropping (display) dimensions. If non-null, *coded_size receives the
+// pre-cropping dimensions actually allocated by the decoder.
 Error parse_sps_for_hvcC_configuration(const uint8_t* sps, size_t size,
                                        HEVCDecoderConfigurationRecord* inout_config,
-                                       int* width, int* height);
+                                       uint32_t* width, uint32_t* height,
+                                       ImageSize* coded_size = nullptr);

 #endif
diff --git a/libheif/codecs/hevc_dec.cc b/libheif/codecs/hevc_dec.cc
index a8a24bb7..5d2bb324 100644
--- a/libheif/codecs/hevc_dec.cc
+++ b/libheif/codecs/hevc_dec.cc
@@ -22,6 +22,7 @@
 #include "hevc_boxes.h"
 #include "error.h"
 #include "context.h"
+#include "plugins/nalu_utils.h"

 #include <string>

@@ -50,6 +51,32 @@ int Decoder_HEVC::get_chroma_bits_per_pixel() const
 }


+Result<std::optional<ImageSize>> Decoder_HEVC::get_coded_image_size_from_config() const
+{
+  const auto& nal_arrays = m_hvcC->get_configuration().m_nal_array;
+
+  for (const auto& arr : nal_arrays) {
+    if (arr.m_NAL_unit_type != HEVC_NAL_UNIT_SPS_NUT || arr.m_nal_units.empty()) {
+      continue;
+    }
+
+    const std::vector<uint8_t>& sps = arr.m_nal_units[0];
+    HEVCDecoderConfigurationRecord scratch = m_hvcC->get_configuration();
+    uint32_t cropped_w = 0, cropped_h = 0;
+    ImageSize coded{};
+    Error e = parse_sps_for_hvcC_configuration(sps.data(), sps.size(), &scratch,
+                                               &cropped_w, &cropped_h, &coded);
+    if (e) {
+      return e;
+    }
+
+    return std::optional<ImageSize>{coded};
+  }
+
+  return std::optional<ImageSize>{};
+}
+
+
 Error Decoder_HEVC::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const
 {
   *out_chroma = (heif_chroma) (m_hvcC->get_configuration().chroma_format);
diff --git a/libheif/codecs/hevc_dec.h b/libheif/codecs/hevc_dec.h
index ac4e67e0..7c0a9a86 100644
--- a/libheif/codecs/hevc_dec.h
+++ b/libheif/codecs/hevc_dec.h
@@ -47,6 +47,8 @@ public:

   Result<std::vector<uint8_t>> read_bitstream_configuration_data() const override;

+  Result<std::optional<ImageSize>> get_coded_image_size_from_config() const override;
+
 private:
   const std::shared_ptr<const Box_hvcC> m_hvcC;
 };
diff --git a/libheif/codecs/hevc_enc.cc b/libheif/codecs/hevc_enc.cc
index e06c282a..293b27c6 100644
--- a/libheif/codecs/hevc_enc.cc
+++ b/libheif/codecs/hevc_enc.cc
@@ -49,8 +49,8 @@ Result<Encoder::CodedImageData> Encoder_HEVC::encode(const std::shared_ptr<HeifP
                  err.message);
   }

-  int encoded_width = 0;
-  int encoded_height = 0;
+  uint32_t encoded_width = 0;
+  uint32_t encoded_height = 0;

   for (;;) {
     uint8_t* data;
@@ -104,8 +104,8 @@ Result<Encoder::CodedImageData> Encoder_HEVC::encode(const std::shared_ptr<HeifP
                                         &check_encoded_width,
                                         &check_encoded_height);

-    assert((int)check_encoded_width == encoded_width);
-    assert((int)check_encoded_height == encoded_height);
+    assert(check_encoded_width == encoded_width);
+    assert(check_encoded_height == encoded_height);
   }

   codedImage.codingConstraints.intra_pred_used = true;
diff --git a/libheif/codecs/hevc_enc.h b/libheif/codecs/hevc_enc.h
index 20c7cfaa..dc57fcc9 100644
--- a/libheif/codecs/hevc_enc.h
+++ b/libheif/codecs/hevc_enc.h
@@ -66,8 +66,8 @@ private:
   std::shared_ptr<class Box_hvcC> m_hvcC;
   bool m_hvcC_sent = false;

-  int m_encoded_image_width = 0;
-  int m_encoded_image_height = 0;
+  uint32_t m_encoded_image_width = 0;
+  uint32_t m_encoded_image_height = 0;

   std::optional<CodedImageData> m_current_output_data;

diff --git a/libheif/codecs/vvc_boxes.cc b/libheif/codecs/vvc_boxes.cc
index 44b6b5ec..104c1a78 100644
--- a/libheif/codecs/vvc_boxes.cc
+++ b/libheif/codecs/vvc_boxes.cc
@@ -20,6 +20,7 @@

 #include "vvc_boxes.h"
 #include "hevc_boxes.h"
+#include "codecs/decoder.h"
 #include "file.h"
 #include <cstring>
 #include <string>
@@ -213,6 +214,18 @@ void Box_vvcC::append_nal_data(const std::vector<uint8_t>& nal)
 }


+const std::vector<uint8_t>* Box_vvcC::get_first_nal_of_type(uint8_t nal_type) const
+{
+  for (const auto& arr : m_nal_array) {
+    if (arr.m_NAL_unit_type == nal_type && !arr.m_nal_units.empty()) {
+      return &arr.m_nal_units[0];
+    }
+  }
+
+  return nullptr;
+}
+
+
 void Box_vvcC::append_nal_data(const uint8_t* data, size_t size)
 {
   std::vector<uint8_t> nal;
@@ -402,7 +415,8 @@ std::string Box_vvcC::dump(Indent& indent) const

 Error parse_sps_for_vvcC_configuration(const uint8_t* sps, size_t size,
                                        Box_vvcC::configuration* config,
-                                       int* width, int* height)
+                                       uint32_t* width, uint32_t* height,
+                                       ImageSize* coded_size)
 {
   // remove start-code emulation bytes from SPS header stream

@@ -486,8 +500,8 @@ Error parse_sps_for_vvcC_configuration(const uint8_t* sps, size_t size,
     reader.skip_bits(1); // sps_res_change_in_clvs_allowed_flag
   }

-  int sps_pic_width_max_in_luma_samples;
-  int sps_pic_height_max_in_luma_samples;
+  uint32_t sps_pic_width_max_in_luma_samples;
+  uint32_t sps_pic_height_max_in_luma_samples;

   bool success;
   success = reader.get_uvlc(&sps_pic_width_max_in_luma_samples);
@@ -498,6 +512,11 @@ Error parse_sps_for_vvcC_configuration(const uint8_t* sps, size_t size,
   *width = sps_pic_width_max_in_luma_samples;
   *height = sps_pic_height_max_in_luma_samples;

+  if (coded_size) {
+    coded_size->width = *width;
+    coded_size->height = *height;
+  }
+
   if (sps_pic_width_max_in_luma_samples > 0xFFFF ||
       sps_pic_height_max_in_luma_samples > 0xFFFF) {
     return {heif_error_Encoding_error,
@@ -510,11 +529,22 @@ Error parse_sps_for_vvcC_configuration(const uint8_t* sps, size_t size,

   int sps_conformance_window_flag = reader.get_bits(1);
   if (sps_conformance_window_flag) {
-    int left,right,top,bottom;
+    uint32_t left, right, top, bottom;
     reader.get_uvlc(&left);
     reader.get_uvlc(&right);
     reader.get_uvlc(&top);
     reader.get_uvlc(&bottom);
+
+    // SubWidthC / SubHeightC per ITU-T H.266 Table 5-1, indexed by chroma_format_idc.
+    uint32_t subWidthC = 1, subHeightC = 1;
+    switch (config->chroma_format_idc) {
+      case 1: subWidthC = 2; subHeightC = 2; break;  // 4:2:0
+      case 2: subWidthC = 2; subHeightC = 1; break;  // 4:2:2
+      default: break;                                // mono / 4:4:4
+    }
+
+    *width  -= subWidthC  * (left + right);
+    *height -= subHeightC * (top + bottom);
   }

   bool sps_subpic_info_present_flag = reader.get_bits(1);
@@ -522,7 +552,7 @@ Error parse_sps_for_vvcC_configuration(const uint8_t* sps, size_t size,
     assert(false); // TODO
   }

-  int bitDepth_minus8;
+  uint32_t bitDepth_minus8;
   success = reader.get_uvlc(&bitDepth_minus8);
   (void)success;

diff --git a/libheif/codecs/vvc_boxes.h b/libheif/codecs/vvc_boxes.h
index 6a4a9b1a..5dd7f689 100644
--- a/libheif/codecs/vvc_boxes.h
+++ b/libheif/codecs/vvc_boxes.h
@@ -85,6 +85,10 @@ public:
   void append_nal_data(const std::vector<uint8_t>& nal);
   void append_nal_data(const uint8_t* data, size_t size);

+  // Returns the bytes of the first NAL unit of the requested type stored in
+  // the configuration record, or nullptr if none is present.
+  const std::vector<uint8_t>* get_first_nal_of_type(uint8_t nal_type) const;
+
   Error write(StreamWriter& writer) const override;

 protected:
@@ -114,8 +118,14 @@ public:
 };


+struct ImageSize;
+
+// Parses a VVC SPS NAL unit. *width / *height return the post-conformance-
+// window cropping (display) dimensions. If non-null, *coded_size receives the
+// pre-cropping dimensions actually allocated by the decoder.
 Error parse_sps_for_vvcC_configuration(const uint8_t* sps, size_t size,
                                        Box_vvcC::configuration* inout_config,
-                                       int* width, int* height);
+                                       uint32_t* width, uint32_t* height,
+                                       ImageSize* coded_size = nullptr);

 #endif // LIBHEIF_VVC_BOXES_H
diff --git a/libheif/codecs/vvc_dec.cc b/libheif/codecs/vvc_dec.cc
index 3c929713..d363cf6a 100644
--- a/libheif/codecs/vvc_dec.cc
+++ b/libheif/codecs/vvc_dec.cc
@@ -22,6 +22,7 @@
 #include "vvc_boxes.h"
 #include "error.h"
 #include "context.h"
+#include "plugins/nalu_utils.h"

 #include <string>

@@ -56,6 +57,26 @@ int Decoder_VVC::get_chroma_bits_per_pixel() const
 }


+Result<std::optional<ImageSize>> Decoder_VVC::get_coded_image_size_from_config() const
+{
+  const std::vector<uint8_t>* sps = m_vvcC->get_first_nal_of_type(VVC_NAL_UNIT_SPS_NUT);
+  if (!sps || sps->empty()) {
+    return std::optional<ImageSize>{};
+  }
+
+  Box_vvcC::configuration scratch = m_vvcC->get_configuration();
+  uint32_t cropped_w = 0, cropped_h = 0;
+  ImageSize coded{};
+  Error e = parse_sps_for_vvcC_configuration(sps->data(), sps->size(), &scratch,
+                                             &cropped_w, &cropped_h, &coded);
+  if (e) {
+    return e;
+  }
+
+  return std::optional<ImageSize>{coded};
+}
+
+
 Error Decoder_VVC::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const
 {
   *out_chroma = (heif_chroma) (m_vvcC->get_configuration().chroma_format_idc);
diff --git a/libheif/codecs/vvc_dec.h b/libheif/codecs/vvc_dec.h
index 0d55b1c3..2ad19344 100644
--- a/libheif/codecs/vvc_dec.h
+++ b/libheif/codecs/vvc_dec.h
@@ -47,6 +47,8 @@ public:

   Result<std::vector<uint8_t>> read_bitstream_configuration_data() const override;

+  Result<std::optional<ImageSize>> get_coded_image_size_from_config() const override;
+
 private:
   const std::shared_ptr<const Box_vvcC> m_vvcC;
 };
diff --git a/libheif/codecs/vvc_enc.cc b/libheif/codecs/vvc_enc.cc
index 96930f22..32495386 100644
--- a/libheif/codecs/vvc_enc.cc
+++ b/libheif/codecs/vvc_enc.cc
@@ -50,8 +50,8 @@ Result<Encoder::CodedImageData> Encoder_VVC::encode(const std::shared_ptr<HeifPi
                  err.message);
   }

-  int encoded_width = 0;
-  int encoded_height = 0;
+  uint32_t encoded_width = 0;
+  uint32_t encoded_height = 0;

   for (;;) {
     uint8_t* data;
diff --git a/libheif/codecs/vvc_enc.h b/libheif/codecs/vvc_enc.h
index 92a2950c..b629816d 100644
--- a/libheif/codecs/vvc_enc.h
+++ b/libheif/codecs/vvc_enc.h
@@ -67,8 +67,8 @@ private:
   std::shared_ptr<class Box_vvcC> m_vvcC;
   bool m_vvcC_sent = false;

-  int m_encoded_image_width = 0;
-  int m_encoded_image_height = 0;
+  uint32_t m_encoded_image_width = 0;
+  uint32_t m_encoded_image_height = 0;
   uint16_t m_avg_frame_rate = 0;

   std::optional<CodedImageData> m_current_output_data;
diff --git a/libheif/plugins/decoder_webcodecs.cc b/libheif/plugins/decoder_webcodecs.cc
index 21f2ae93..44d3489d 100644
--- a/libheif/plugins/decoder_webcodecs.cc
+++ b/libheif/plugins/decoder_webcodecs.cc
@@ -205,7 +205,7 @@ static std::vector<uint8_t> remove_start_code_emulation2(const uint8_t* sps, siz

 Error parse_sps_for_hvcC_configuration2(const uint8_t* sps, size_t size,
                                        HEVCDecoderConfigurationRecord* config,
-                                       int* width, int* height)
+                                       uint32_t* width, uint32_t* height)
 {
   // remove start-code emulation bytes from SPS header stream

@@ -269,7 +269,7 @@ Error parse_sps_for_hvcC_configuration2(const uint8_t* sps, size_t size,

   // --- SPS continued ---

-  int dummy, value;
+  uint32_t dummy, value;
   reader.get_uvlc(&dummy); // skip seq_parameter_seq_id

   reader.get_uvlc(&value);
@@ -284,15 +284,15 @@ Error parse_sps_for_hvcC_configuration2(const uint8_t* sps, size_t size,

   bool conformance_window = reader.get_bits(1);
   if (conformance_window) {
-    int left, right, top, bottom;
+    uint32_t left, right, top, bottom;
     reader.get_uvlc(&left);
     reader.get_uvlc(&right);
     reader.get_uvlc(&top);
     reader.get_uvlc(&bottom);

-    //printf("conformance borders: %d %d %d %d\n",left,right,top,bottom);
+    //printf("conformance borders: %u %u %u %u\n",left,right,top,bottom);

-    int subH = 1, subV = 1;
+    uint32_t subH = 1, subV = 1;
     if (config->chroma_format == 1) {
       subV = 2;
       subH = 2;
@@ -671,7 +671,7 @@ static struct heif_error webcodecs_decode_image(void* decoder_raw,
   }

   HEVCDecoderConfigurationRecord config;
-  int w, h;
+  uint32_t w, h;
   Error err = parse_sps_for_hvcC_configuration2(sps_nal_unit.data.data(), sps_nal_unit.data.size(), &config, &w, &h);
   if (err != Error::Ok) {
     return {heif_error_Decoder_plugin_error,