CVE-2026-78963
Overview
Files Changed
media/gpu/av1_decoder.cc
Patch
From 8f06b14492a6591362290aa978f5f61484b26464 Mon Sep 17 00:00:00 2001 From: Ted Meyer <[email protected]> Date: Mon, 20 Jul 2026 17:18:39 -0700 Subject: [PATCH] Bounds check av1 decoder We have a limit of 32k pixels for any given dimension, but av1 allows 64k. We should fail to decode if it's too big. Fixed: 536428842 Change-Id: Iedefeef7fac680674820553ceab52bf5f31ff435 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8127838 Reviewed-by: Syed AbuTalib <[email protected]> Auto-Submit: Ted (Chromium) Meyer <[email protected]> Commit-Queue: Syed AbuTalib <[email protected]> Commit-Queue: Ted (Chromium) Meyer <[email protected]> Cr-Commit-Position: refs/heads/main@{#1665064} --- diff --git a/media/gpu/av1_decoder.cc b/media/gpu/av1_decoder.cc index f3b862ed..c84473a 100644 --- a/media/gpu/av1_decoder.cc +++ b/media/gpu/av1_decoder.cc @@ -354,7 +354,16 @@ gfx::Rect new_visible_rect( base::strict_cast<int>(current_frame_header_->width), base::strict_cast<int>(current_frame_header_->height)); + DCHECK(!new_frame_size.IsEmpty()); + if (new_frame_size.width() > limits::kMaxDimension || + new_frame_size.height() > limits::kMaxDimension || + new_frame_size.GetCheckedArea().ValueOrDefault( + std::numeric_limits<int>::max()) > limits::kMaxCanvas) { + DVLOG(1) << "AV1 max_frame_size " << new_frame_size.ToString() + << " exceeds media::limits"; + return kDecodeError; + } if (!gfx::Rect(new_frame_size).Contains(new_visible_rect)) { DVLOG(1) << "Render size exceeds picture size. render size: " << new_visible_rect.ToString()
Original Bug Report
Potential Unbounded AV1 max_frame_width/height Reaches GPU Drivers (Missing kMaxDimension Check)
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: Chromium’s AV1Decoder fails to validate libgav1-parsed max_frame_width and max_frame_height (up to 65536) against limits::kMaxDimension during mid-stream resolution changes. These unvalidated dimensions reach hardware decode drivers (via vaCreateContext and ID3D11VideoDevice::CreateVideoDecoder / DXVA_PicParams_AV1) in the sandboxed GPU or OOP-VD processes. This may trigger a vendor driver-side integer overflow resulting in an undersized allocation and subsequent out-of-bounds write.
Affected files:
media/gpu/av1_decoder.ccmedia/gpu/windows/d3d11_av1_accelerator.ccmedia/gpu/windows/d3d11_decoder_configurator.ccmedia/gpu/windows/d3d11_video_decoder.ccmedia/gpu/vaapi/vaapi_video_decoder.cc
Estimated timestamp from git blame: 2020-11-14
1. Summary of the Issue (Meant for Human Triage)
A potential vulnerability exists in Chromium’s AV1 video decoder implementation where the hardware-decode picture size is derived from attacker-controlled sequence headers without validation against Chromium’s platform limits. Specifically, media::AV1Decoder sets the frame_size_ property directly from the sequence header’s max_frame_width and max_frame_height fields as parsed by the libgav1 library. The AV1 specification permits these fields to be as large as 65536x65536.
During a mid-stream resolution change (kConfigChange), AV1Decoder fails to restrict these parsed dimensions against standard Chromium video bounds, such as media::limits::kMaxDimension (32767) or media::limits::kMaxCanvas (2^29 - 1). The unvalidated dimensions are subsequently passed directly to low-level platform APIs in sandboxed hardware-decode processes:
- On Windows (sandboxed GPU process): Passed to
ID3D11VideoDevice::CreateVideoDecoder()andID3D12VideoDevice::CreateVideoDecoderHeap(). Furthermore, the D3D11 accelerator delegate verbatim copies the unbounded sizes into theDXVA_PicParams_AV1struct. - On Linux/ChromeOS (sandboxed OOP-VD utility process): Passed to
vaCreateContext()andvaCreateSurfaces().
Because these parameters reach proprietary vendor kernel-mode and user-mode drivers without prior bounding, they present a risk of driver-side integer overflows (e.g., during buffer capacity calculations like width * height * bytes_per_pixel wrapping 32-bit limits). This can lead to undersized memory allocations and consequent out-of-bounds (OOB) writes within the driver. The issue does not apply to Android, as Android platforms utilize MediaCodecVideoDecoder rather than media::AV1Decoder.
2. Proof-of-Concept & Detailed Execution Flow
The following sequence demonstrates the execution flow from the bitstream parsing stage down to the proprietary graphics driver sinks.
(Note: These are potential steps tracing the exact code paths, as our static analysis tooling does not execute dynamic PoCs.)
Potential Attacker Steps to Trigger:
- Decoder Setup: An attacker uses the WebCodecs API (e.g.,
VideoDecoder.configure()) to initialize hardware-accelerated decoding with valid, small initial dimensions (e.g., 128x128). This passes the initialVideoDecoderConfig::IsValidConfig()check. - Payload Delivery: The attacker queues a maliciously crafted AV1 Temporal Unit containing a Sequence Header OBU and a Frame Header OBU.
- Sequence Header Forgery: The Sequence Header is configured with
frame_width_bits_minus_1= 15,frame_height_bits_minus_1= 15,max_frame_width_minus_1= 65535, andmax_frame_height_minus_1= 65535. Additionally, the attacker setsseq_level_idx[0] = 31to evade level conformance checks. - Frame Header Restraint: The Frame Header OBU is configured with
frame_size_override_flag = 1and actual frame dimensions of 128x128 to pass frame bounding checks.
Detailed Execution Flow:
Step 1: Parser Level (libgav1)
At third_party/libgav1/src/src/obu_parser.cc:485-492, libgav1 parses the sequence header OBU:
OBU_READ_LITERAL_OR_FAIL(4);
sequence_header.frame_width_bits = 1 + scratch; // ∈ [1, 16]
OBU_READ_LITERAL_OR_FAIL(4);
sequence_header.frame_height_bits = 1 + scratch; // ∈ [1, 16]
OBU_READ_LITERAL_OR_FAIL(sequence_header.frame_width_bits);
sequence_header.max_frame_width = static_cast<int32_t>(1 + scratch); // ∈ [1, 65536]
OBU_READ_LITERAL_OR_FAIL(sequence_header.frame_height_bits);
sequence_header.max_frame_height = static_cast<int32_t>(1 + scratch); // ∈ [1, 65536]
To bypass CheckLevelConformance (obu_parser.cc:99-183), the attacker’s seq_level_idx >= 20 (e.g., 31) triggers an early return of kStatusOk (obu_parser.cc:136). Frame-specific conformance checks (:636-640 and :688) pass because the actual frame size remains 128x128.
Step 2: AV1Decoder Resolution Transition
In media/gpu/av1_decoder.cc:337-349, AV1Decoder::DecodeInternal() processes the new sequence header:
const gfx::Size new_frame_size(
base::strict_cast<int>(new_sequence_header.max_frame_width),
base::strict_cast<int>(new_sequence_header.max_frame_height));
gfx::Rect new_visible_rect(
base::strict_cast<int>(current_frame_header_->width),
base::strict_cast<int>(current_frame_header_->height));
DCHECK(!new_frame_size.IsEmpty());
if (!gfx::Rect(new_frame_size).Contains(new_visible_rect)) {
...
}
No check is performed against media::limits::kMaxDimension. Since a 65536x65536 rectangle logically contains a 128x128 rectangle, the code continues. At av1_decoder.cc:396, frame_size_ is updated:
frame_size_ = new_frame_size;
And the function returns kConfigChange (:404).
Step 3a: Sink - D3D11/D3D12 (Windows GPU Process)
In media/gpu/windows/d3d11_video_decoder.cc:667-690, the config’s coded size is blindly updated using the new, unchecked dimensions (config_.set_coded_size(new_coded_size)).
This triggers D3D11DecoderConfigurator::SetUpDecoderDescriptor (media/gpu/windows/d3d11_decoder_configurator.cc:218-225):
void D3D11DecoderConfigurator::SetUpDecoderDescriptor(const gfx::Size& coded_size) {
decoder_desc_ = {};
decoder_desc_.Guid = decoder_guid_;
decoder_desc_.SampleWidth = coded_size.width(); // 65536
decoder_desc_.SampleHeight = coded_size.height(); // 65536
decoder_desc_.OutputFormat = dxgi_format_;
}
This descriptor is dispatched to the underlying driver via CreateVideoDecoder() (media/gpu/windows/d3d11_video_decoder_wrapper.cc:381).
Simultaneously, D3D11AV1Accelerator::FillPicParams verbatim copies the fields into DXVA_PicParams_AV1 (media/gpu/windows/d3d11_av1_accelerator.cc:174-177):
pp->width = frame_header.width;
pp->height = frame_header.height;
pp->max_width = seq_header.max_frame_width; // Verbatim copy: 65536
pp->max_height = seq_header.max_frame_height; // Verbatim copy: 65536
These are directly submitted to the vendor’s User-Mode Driver (UMD) via ID3D11VideoContext::SubmitDecoderBuffers(), presenting a significant risk of internal driver allocations undersizing their buffers.
Step 3b: Sink - VA-API (Linux/ChromeOS OOP-VD process)
In media/gpu/vaapi/vaapi_video_decoder.cc:698, decoder_->GetPicSize() is retrieved. At vaapi_video_decoder.cc:795, the wrapper creates a new driver context:
if (!vaapi_wrapper_->CreateContext(decoder_pic_size)) { ... }
This triggers vaCreateContext in media/gpu/vaapi/vaapi_wrapper.cc:2560-2563:
VAStatus va_res = vaCreateContext(
va_display_, va_config_id_, picture_size.width(), picture_size.height(), // 65536, 65536
flag, empty_va_surfaces_ids_pointer, empty_va_surfaces_ids_size,
&va_context_id_);
Suggested Fix
In media/gpu/av1_decoder.cc::DecodeInternal(), prior to verifying the container boundaries, explicitly enforce media::limits::kMaxDimension limits against the parsed sequence headers.
const gfx::Size new_frame_size(
base::strict_cast<int>(new_sequence_header.max_frame_width),
base::strict_cast<int>(new_sequence_header.max_frame_height));
if (new_frame_size.width() > media::limits::kMaxDimension ||
new_frame_size.height() > media::limits::kMaxDimension) {
return kDecodeError;
}
3. Technical Verification Details (Automated Audit Logs)
Prior Critic Verdict and Reasoning
* Severity: High (S1)
* Brief Notes / Reasoning:
The report accurately identifies that media::AV1Decoder::GetPicSize() can return dimensions up to 65536x65536 because libgav1::ObuSequenceHeader::max_frame_width and max_frame_height are uncontrolled by the Chromium wrapper and lack validation against media::limits::kMaxDimension (32767). The IsValidConfig() check in MojoVideoDecoderService is bypassed since the malicious sequence header triggers an internal kConfigChange that applies the new unvalidated size directly.
An attacker can trigger this via a malicious AV1 bitstream (e.g., <video> or WebCodecs). The unvalidated size reaches multiple HW-decode APIs: vaCreateContext() on Linux/CrOS, and ID3D11VideoDevice::CreateVideoDecoder() on Windows.
Crucially, on Windows, the D3D11 accelerator delegate (d3d11_av1_accelerator.cc) copies seq_header.max_frame_width verbatim into the DXVA_PicParams_AV1 struct passed to the driver.
The report rates this as Medium (S2) arguing that driver-side consequences (OOB vs clean fail) are unconfirmed. However, the KB explicitly categorizes this as High (S1): '- Bitstream field passed unvalidated to a HW-decode driver struct: a [media parser] field read ... with no IN_RANGE_OR_RETURN, copied verbatim by an accelerator delegate (*_vaapi_video_decoder_delegate.cc, d3d11_*_accelerator.cc) into VAPictureParameterBuffer* / DXVA_PicParams_* ... Do NOT reject for "no ASAN report" — the corruption is driver-side. PoC = the bad value at the delegate copy step.'
The vulnerability runs in the sandboxed GPU process (Windows) and sandboxed OOP-VD process (Linux/CrOS). It does not reach Android's unsandboxed GPU process, as Android uses MediaCodecVideoDecoder rather than media::AV1Decoder, precluding a Critical (S0) rating. Consequently, severity is High (S1).
Audit Logs & Code Reachability Proofs
libgav1Array Checks: The validator confirmed viacodebase_investigatorthatkLevelInfois an array of size 20 inobu_parser.cc. Supplying aseq_level_idx≥ 20 successfully causesCheckLevelConformanceto abort processing immediately and returnkStatusOkwithout bounds validation (obu_parser.cc:136).- Absence of Limit Clamping: The validator confirmed via
codebase_investigatorthatAV1Decoder::DecodeInternal()does not invokelimits::kMaxDimension(media/base/limits.h). - Config Validation Skip on Windows: Code traces from
d3d11_video_decoder.cc:660-720verify thatconfig_.set_coded_size(new_coded_size)runs inside thekConfigChangehandler block with no invocation ofVideoDecoderConfig::IsValidConfig(). The bad state proceeds directly intoResetD3DVideoDecoder(), demonstrating an irrefutable path to descriptor poisoning. - No Downstream Clamps in VA-API: The validator checked
media/gpu/vaapi/vaapi_wrapper.cc:2543-2578and confirmed thatpicture_size.width()andpicture_size.height()are passed without secondary validation or clamping tovaCreateContext(). - Platform Exclusion: Investigation into
media/gpu/args.gni(use_av1_hw_decoder) andmedia/gpu/BUILD.gndemonstrates Android usesMediaCodecVideoDecoderand therefore excludesmedia::AV1Decoder. The issue’s ceiling correctly falls to the Sandboxed GPU process boundaries (S1/High).
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.