Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Codecs
DescriptionInsufficient validation of untrusted input in Codecs
ComponentCodecs
Bug ClassLogic Error
Tracker536470854
Fix commit5dc7ba4f4e47 (chromium/src) +33/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-06

Changed Functions

FunctionChangeNotes
if
media/parsers/h265_parser.cc
modified
TEST_F
media/parsers/h265_parser_unittest.cc
modified

Files Changed

  • media/gpu/h265_decoder.cc
  • media/parsers/h265_parser.cc
  • media/parsers/h265_parser_unittest.cc
From 5dc7ba4f4e4770e36b567afbf457740e227d9edb Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <eugene@chromium.org>
Date: Tue, 28 Jul 2026 10:01:55 -0700
Subject: [PATCH] media: Reject HEVC non-first slice segment when prior slice is missing

Disallow non-first slice segments (first_slice_segment_in_pic_flag == 0)
when no prior slice header exists (e.g. after mid-stream SPS/PPS
updates). Enforce this in H265Parser::ParseSliceHeader() when
validate_extended_bitstream_ is set, and in
H265Decoder::PreprocessCurrentSlice() when curr_pic_ is null.

Bug: 536470854
Change-Id: Ie7f159fbb9cdb229f516f0e46699a22aedefa184
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8160680
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Commit-Queue: Eugene Zemtsov <eugene@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1669582}
---

diff --git a/media/gpu/h265_decoder.cc b/media/gpu/h265_decoder.cc
index 1a1b0a4..750a4ad 100644
--- a/media/gpu/h265_decoder.cc
+++ b/media/gpu/h265_decoder.cc
@@ -703,6 +703,10 @@
       return result;
 
     DCHECK(!curr_pic_);
+  } else if (!curr_pic_) {
+    DVLOG(1) << "Received slice segment with first_slice_segment_in_pic_flag "
+             << "equal to 0 without an active picture";
+    return H265Accelerator::Status::kFail;
   }
 
   return H265Accelerator::Status::kOk;
diff --git a/media/parsers/h265_parser.cc b/media/parsers/h265_parser.cc
index 22d15ce..8d65703 100644
--- a/media/parsers/h265_parser.cc
+++ b/media/parsers/h265_parser.cc
@@ -1156,6 +1156,11 @@
       std::min(shdr->temporal_id, sps->sps_max_sub_layers_minus1);
 
   if (!shdr->first_slice_segment_in_pic_flag) {
+    if (validate_extended_bitstream_ && !prior_shdr) {
+      DVLOG(1) << "First slice segment in picture must have "
+               << "first_slice_segment_in_pic_flag equal to 1";
+      return kInvalidStream;
+    }
     if (pps->dependent_slice_segments_enabled_flag)
       READ_BOOL_OR_RETURN(&shdr->dependent_slice_segment_flag);
     READ_BITS_OR_RETURN(base::bits::Log2Ceiling(sps->pic_size_in_ctbs_y),
diff --git a/media/parsers/h265_parser_unittest.cc b/media/parsers/h265_parser_unittest.cc
index 5f0274c..2b97717 100644
--- a/media/parsers/h265_parser_unittest.cc
+++ b/media/parsers/h265_parser_unittest.cc
@@ -924,4 +924,28 @@
             H265Parser::kOk);
 }
 
+TEST_F(H265CrossSliceTest, RejectsNonFirstSliceSegmentWithoutPriorSliceHeader) {
+  H26xAnnexBBitstreamBuilder builder;
+  BuildSpsAndPps(builder);
+  AppendSecondSlice(builder, H265NALU::IDR_W_RADL);
+  builder.Flush();
+  parser_.SetStream(builder.data());
+
+  H265NALU nalu;
+  int sps_id;
+  int pps_id;
+  ASSERT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk);
+  ASSERT_EQ(nalu.nal_unit_type, H265NALU::SPS_NUT);
+  ASSERT_EQ(parser_.ParseSPS(&sps_id), H265Parser::kOk);
+  ASSERT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk);
+  ASSERT_EQ(nalu.nal_unit_type, H265NALU::PPS_NUT);
+  ASSERT_EQ(parser_.ParsePPS(nalu, &pps_id), H265Parser::kOk);
+
+  ASSERT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk);
+  ASSERT_EQ(nalu.nal_unit_type, H265NALU::IDR_W_RADL);
+  H265SliceHeader shdr;
+  EXPECT_EQ(parser_.ParseSliceHeader(nalu, &shdr, nullptr),
+            H265Parser::kInvalidStream);
+}
+
 }  // namespace media
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/parsers/h265_parser_unittest.cc b/media/parsers/h265_parser_unittest.cc
index 5f0274c..2b97717 100644
--- a/media/parsers/h265_parser_unittest.cc
+++ b/media/parsers/h265_parser_unittest.cc
@@ -924,4 +924,28 @@
             H265Parser::kOk);
 }
 
+TEST_F(H265CrossSliceTest, RejectsNonFirstSliceSegmentWithoutPriorSliceHeader) {
+  H26xAnnexBBitstreamBuilder builder;
+  BuildSpsAndPps(builder);
+  AppendSecondSlice(builder, H265NALU::IDR_W_RADL);
+  builder.Flush();
+  parser_.SetStream(builder.data());
+
+  H265NALU nalu;
+  int sps_id;
+  int pps_id;
+  ASSERT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk);
+  ASSERT_EQ(nalu.nal_unit_type, H265NALU::SPS_NUT);
+  ASSERT_EQ(parser_.ParseSPS(&sps_id), H265Parser::kOk);
+  ASSERT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk);
+  ASSERT_EQ(nalu.nal_unit_type, H265NALU::PPS_NUT);
+  ASSERT_EQ(parser_.ParsePPS(nalu, &pps_id), H265Parser::kOk);
+
+  ASSERT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk);
+  ASSERT_EQ(nalu.nal_unit_type, H265NALU::IDR_W_RADL);
+  H265SliceHeader shdr;
+  EXPECT_EQ(parser_.ParseSliceHeader(nalu, &shdr, nullptr),
+            H265Parser::kInvalidStream);
+}
+
 }  // namespace media
Loading diff…

Original Bug Report

reported by aw...@chromium.org

Potential High: H265Decoder skips ProcessPPS on first_slice_segment_in_pic_flag=0 leading to driver OOB write

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic bypass in H265Decoder::Decode allows a malformed HEVC bitstream to skip surface reallocation while applying a newly parsed, larger SPS. This leads to submitting mismatched frame parameters (e.g. 8192x8192) to hardware-accelerated drivers against a stale-sized (e.g. 64x64) backing surface. The underlying graphics drivers then write out-of-bounds in GPU memory based on these maliciously large dimensions.

Affected files:

  • media/gpu/h265_decoder.cc
  • media/parsers/h265_parser.cc
  • media/gpu/vaapi/h265_vaapi_video_decoder_delegate.cc
  • media/gpu/windows/d3d11_h265_accelerator.cc
  • media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc

Estimated timestamp from git blame: 2026-03-09

1. Summary of the Issue (Meant for Human Triage)

A potential logic flaw in media/gpu/h265_decoder.cc allows a malformed/spec-illegal (but parser-accepted) HEVC bitstream to trigger mismatched picture dimensions between the high-level decoder state and the backend hardware-accelerated drivers.

Specifically, the decoder gates calls to ProcessPPS() (the only execution path that re-evaluates picture dimensions, issues kConfigChange, and manages hardware context re-allocations) on the condition that curr_slice_hdr_->first_slice_segment_in_pic_flag is non-zero. If an incoming slice has first_slice_segment_in_pic_flag set to 0, but is parsed under conditions where curr_pic_ == nullptr and last_slice_hdr_ == nullptr (which occurs naturally after a preceding mid-stream SPS/PPS overwrite calls FinishPrevFrameIfPresent()), ProcessPPS() is completely bypassed.

As a result, the decoder proceeds to allocate a picture surface from the stale, smaller surface pool (e.g., 64x64) but extracts the newly overwritten, larger SPS dimensions (e.g., 8192x8192) in StartNewFrame(). It then submits these mismatched parameters verbatim to the underlying accelerator delegate (VA-API, D3D11VA, or V4L2). Because driver-side structures calculate memory regions, macroblock buffers, and motion vector offsets from these newly parsed large dimensions while operating on the smaller physical backing surface, this causes an out-of-bounds (OOB) write in driver-allocated GPU memory.

This vulnerability is reachable remotely via WebCodecs or MSE (A-SERVER reachable) without requiring a compromised renderer. It affects Windows, macOS, Linux, and ChromeOS where the GPU or OOP-VD utility process is sandboxed (Android is unaffected as it utilizes the stateful MediaCodecVideoDecoder). This represents a High-severity (S1) memory corruption primitive.

2. Proof-of-Concept & Detailed Execution Flow

Note: The following are potential steps that trace the theoretical execution flow to trigger the vulnerability. Our tooling agent does not have the ability to run code, so this sequence relies on static analysis and codebase state machine verification.

Potential Attack Sequence

An attacker delivers a single Annex-B byte stream utilizing a sequence like this:

  1. SPS_NUT: sps_seq_parameter_set_id = 0, pic_width_in_luma_samples = 64, pic_height_in_luma_samples = 64.
  2. PPS_NUT: pps_pic_parameter_set_id = 0, pps_seq_parameter_set_id = 0.
  3. IDR_W_RADL: first_slice_segment_in_pic_flag = 1, slice_pic_parameter_set_id = 0, slice_type = I.
  4. SPS_NUT: sps_seq_parameter_set_id = 0, pic_width_in_luma_samples = 8192, pic_height_in_luma_samples = 8192.
  5. PPS_NUT: pps_pic_parameter_set_id = 0, pps_seq_parameter_set_id = 0.
  6. IDR_W_RADL: first_slice_segment_in_pic_flag = 0, dependent_slice_segment_flag = 0, slice_segment_address = 1, slice_pic_parameter_set_id = 0, slice_type = I.

Code Execution Trace

  1. Initial Config (NALUs 1 & 2): H265Decoder::Decode() parses SPS/PPS. Since curr_pps_id_ == -1, it calls ProcessPPS(), updating pic_size_ = {64, 64} and returning kConfigChange. The hardware decoder context and surface pool allocate at 64x64.
  2. First Picture (NALU 3): Decoded successfully. curr_pps_id_ becomes 0. curr_pic_ references a 64x64 surface. last_slice_hdr_ points to this slice header.
  3. SPS Overwrite (NALU 4):
    • Execution processes H265NALU::SPS_NUT (media/gpu/h265_decoder.cc:450), calling FinishPrevFrameIfPresent().
    • FinishPrevFrameIfPresent() invokes FinishPicture(std::move(curr_pic_), std::move(last_slice_hdr_)) (:1109).
    • The std::move implicitly nullifies both curr_pic_ and last_slice_hdr_ in the decoder state.
    • The parser reads the new SPS_NUT (NALU 4) and overwrites the active SPS entry sps_id = 0 with 8192x8192 dimensions (passing bounds check up to 16,888 at h265_parser.cc:626).
  4. PPS Update (NALU 5): The parser parses the new PPS. At media/gpu/h265_decoder.cc:471, the check if (curr_pps_id_ == -1) evaluates to false because curr_pps_id_ is 0. ProcessPPS() is skipped, meaning pic_size_ remains at the stale 64x64 size and no kConfigChange is returned.
  5. Triggering the Desync (NALU 6):
    • Parser Bypass: The slice header parser (media/parsers/h265_parser.cc:1138) processes NALU 6. Since dependent_slice_segment_flag is 0 (or defaults to 0) and prior_shdr (derived from last_slice_hdr_) is nullptr, the parser bypasses the !prior_shdr check for dependent slices (:1167). Crucially, because prior_shdr is nullptr, it also completely bypasses the cross-slice validation block at media/parsers/h265_parser.cc:1471. The parser accepts the illegal slice segment address against the new, larger SPS.
    • Gate Bypass: In H265Decoder::Decode():
      // media/gpu/h265_decoder.cc:387
      if (curr_slice_hdr_->first_slice_segment_in_pic_flag) {
        bool need_new_buffers = false;
        if (!ProcessPPS(...)) { ... }
        ...
      }
      
      Because first_slice_segment_in_pic_flag == 0, ProcessPPS() is completely bypassed, circumventing the protective check inside ProcessPPS() against configuration changes on non-IRAP pictures (:658).
    • Stale Picture Allocation: The state-machine proceeds to kEnsurePicture. Since curr_pic_ == nullptr, it calls curr_pic_ = accelerator_->CreateH265Picture();. This allocates a 64x64 surface from the stale-sized surface pool.
    • Mismatched Metadata Submission: StartNewFrame() is called. It fetches the active SPS (which is now 8192x8192) but applies the stale visible_rect_ (64x64) to the picture (:1063). It then calls accelerator_->SubmitFrameMetadata(sps, ..., curr_pic_).
  6. Hardware Delegate Sinks (OOB Write):
    • The hardware delegates copy the large SPS dimensions verbatim into hardware driver structs without cross-validating them against the physical backing surface.
    • VA-API: pic_param.pic_width_in_luma_samples = sps->pic_width_in_luma_samples; (h265_vaapi_video_decoder_delegate.cc:109).
    • D3D11: (pic_param.params).PicWidthInMinCbsY = sps->pic_width_in_luma_samples >> min_cb_log2_size_y; (d3d11_h265_accelerator.cc:194).
    • These parameters are submitted via vaRenderPicture() or ID3D11VideoContext::SubmitDecoderBuffers() against the 64x64 surface. The GPU driver proceeds to use the 8192x8192 logical picture sizes to address memory, resulting in an out-of-bounds write in GPU-mapped driver memory.

Suggested Fix

The mismatch occurs because hardware-acceleration delegates blindly trust the sps dimensions passed into SubmitFrameMetadata without validating them against the backing surface size.

  1. Delegate Validation: In H265VaapiVideoDecoderDelegate::SubmitFrameMetadata, D3D11H265Accelerator::SubmitFrameMetadata, and V4L2VideoDecoderDelegateH265::SubmitFrameMetadata, add a strict bounding check comparing the provided sps->pic_width_in_luma_samples and pic_height_in_luma_samples against the physical size of the provided pic surface (e.g., pic->visible_rect().size() or pic->AsVaapiH265Picture()->va_surface()->size()). Return a failure status if they misalign.
  2. Decoder Logic Hardening: Modify H265Decoder::Decode so that ProcessPPS handles checking for pending configuration changes unconditionally when a new SPS/PPS is encountered, rather than relying exclusively on the first_slice_segment_in_pic_flag gate.

3. Technical Verification Details (Automated Audit Logs)

> Severity: High (S1) > Brief Notes / Reasoning: > The report accurately details a logic flaw in H265Decoder::Decode where an attacker-controlled bitstream can bypass surface reconfiguration (ProcessPPS()) by sending an IDR slice with first_slice_segment_in_pic_flag set to 0 immediately following a new SPS. The parser (media/parsers/h265_parser.cc:1158-1175) only validates prior_shdr for dependent slice segments, thereby accepting this spec-invalid structure. > > As verified, this desynchronization results in the accelerator delegate (h265_vaapi_video_decoder_delegate.cc or d3d11_h265_accelerator.cc) feeding the new, large SPS dimensions (e.g. 8192x8192) into the driver struct (e.g. DXVA_PicParams_HEVC.PicWidthInMinCbsY), while the actual backing surface and visible_rect_ remain at the stale size (e.g. 64x64). This produces a driver-side OOB write when the driver renders the 8192x8192 logical picture into a 64x64 surface. > > Per the severity guidelines and KB (GPU validating-layer gap / bitstream field passed unvalidated to HW-decode driver struct), a memory corruption primitive in the GPU process reachable from A-SERVER (WebCodecs/MSE) is High (S1) when sandboxed. H265Decoder is not used on Android (which uses MediaCodecVideoDecoder), avoiding the unsandboxed Critical (S0) tier. The highest reachable tier is the sandboxed Windows GPU process, making this High (S1) severity.

Verifiable Code Reachability & State Assumptions

  • std::move behavior nullifies last_slice_hdr_: Confirmed at media/gpu/h265_decoder.cc:1109. FinishPicture(std::move(curr_pic_), std::move(last_slice_hdr_)) moves the std::unique_ptr, guaranteeing last_slice_hdr_ is nullptr in subsequent processing.
  • Parser prior_shdr bypass: Confirmed at media/parsers/h265_parser.cc:1471. if (prior_shdr && !shdr->first_slice_segment_in_pic_flag) dictates cross-slice picture validation. Because prior_shdr is passed as nullptr (from last_slice_hdr_.get()), validation is skipped.
  • Dependent Slice Default: Confirmed at media/parsers/h265_parser.h:394. bool dependent_slice_segment_flag = false; allows the attacker to omit it and seamlessly bypass !prior_shdr error handling at h265_parser.cc:1167.
  • ProcessPPS Bypass: Confirmed at media/gpu/h265_decoder.cc:387. The if (curr_slice_hdr_->first_slice_segment_in_pic_flag) evaluates to false, unconditionally skipping ProcessPPS() and allowing the 64x64 state variables to persist despite the active 8192x8192 SPS.
  • Verbatim Sink Mapping: Confirmed at media/gpu/vaapi/h265_vaapi_video_decoder_delegate.cc:109 (FROM_SPS_TO_PP(pic_width_in_luma_samples)) and media/gpu/windows/d3d11_h265_accelerator.cc:194. The delegates make zero cross-checks against the underlying VASurfaceID / ID3D11VideoDecoderOutputView size.

Evaluated with Chrome root at commit: b96d2ec58f4f5f92b540a723966b199d6e9951b4


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker