CVE-2026-76035
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/gpu/mac/video_toolbox_av1_accelerator.cc |
modified | |
TEST_Fmedia/gpu/mac/video_toolbox_av1_accelerator_unittest.cc |
modified |
Files Changed
media/gpu/mac/video_toolbox_av1_accelerator.ccmedia/gpu/mac/video_toolbox_av1_accelerator.hmedia/gpu/mac/video_toolbox_av1_accelerator_unittest.cc
Patch
From 8d7b91ee781c387a36d9fe0472f3210c3da6a3f7 Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <eugene@chromium.org>
Date: Mon, 03 Aug 2026 10:38:41 -0700
Subject: [PATCH] media: Validate av1C box and track bit depth & frame size in VT AV1 accelerator
VideoToolboxAV1Accelerator::ProcessFormat() previously failed to
validate the output of
libgav1::ObuParser::GetAV1CodecConfigurationBox(), allowing empty/null
av1C spans to construct malformed CoreMedia format descriptions.
Additionally, bit depth and sequence max frame dimensions were omitted
from format change detection keys, preventing format description updates
on stream parameter transitions. transitions.
Bug: 536439844
Change-Id: I6d3ff4086a4e8ac9470ca0816344dd6e00490b5e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8179644
Commit-Queue: Eugene Zemtsov <eugene@chromium.org>
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1672762}
---
diff --git a/media/gpu/mac/video_toolbox_av1_accelerator.cc b/media/gpu/mac/video_toolbox_av1_accelerator.cc
index 62dd23e..689306a 100644
--- a/media/gpu/mac/video_toolbox_av1_accelerator.cc
+++ b/media/gpu/mac/video_toolbox_av1_accelerator.cc
@@ -188,14 +188,18 @@
break;
}
- // TODO(crbug.com/40936765): Should this be the current frame size, or the
- // sequence max frame size?
gfx::Size coded_size(base::strict_cast<int>(pic.frame_header.width),
base::strict_cast<int>(pic.frame_header.height));
+ gfx::Size max_coded_size(
+ base::strict_cast<int>(sequence_header.max_frame_width),
+ base::strict_cast<int>(sequence_header.max_frame_height));
+
+ int bit_depth = sequence_header.color_config.bitdepth;
// If the parameters have changed, generate a new format.
if (color_space != active_color_space_ || profile != active_profile_ ||
- coded_size != active_coded_size_) {
+ coded_size != active_coded_size_ || bit_depth != active_bit_depth_ ||
+ max_coded_size != active_max_coded_size_) {
active_format_.reset();
// Generate the av1c.
@@ -203,13 +207,17 @@
std::unique_ptr<uint8_t[]> av1c =
libgav1::ObuParser::GetAV1CodecConfigurationBox(
data.data(), data.size(), &av1c_size);
+ if (!av1c || av1c_size < 4) {
+ MEDIA_LOG(ERROR, media_log_.get())
+ << "Failed to extract valid AV1CodecConfigurationBox";
+ return false;
+ }
auto av1c_span =
UNSAFE_TODO(base::span<const uint8_t>(av1c.get(), av1c_size));
// Build a format configuration with AV1 extensions.
base::apple::ScopedCFTypeRef<CFDictionaryRef> format_config =
- CreateFormatExtensions(kCMVideoCodecType_AV1, profile,
- sequence_header.color_config.bitdepth,
+ CreateFormatExtensions(kCMVideoCodecType_AV1, profile, bit_depth,
color_space, av1c_span);
if (!format_config) {
MEDIA_LOG(ERROR, media_log_.get())
@@ -235,6 +243,8 @@
active_color_space_ = color_space;
active_profile_ = profile;
active_coded_size_ = coded_size;
+ active_max_coded_size_ = max_coded_size;
+ active_bit_depth_ = bit_depth;
// Update session configuration.
session_metadata_ = VideoToolboxDecompressionSessionMetadata{
diff --git a/media/gpu/mac/video_toolbox_av1_accelerator.h b/media/gpu/mac/video_toolbox_av1_accelerator.h
index 2f38bdb6..2f6b183 100644
--- a/media/gpu/mac/video_toolbox_av1_accelerator.h
+++ b/media/gpu/mac/video_toolbox_av1_accelerator.h
@@ -66,6 +66,8 @@
VideoColorSpace active_color_space_;
VideoCodecProfile active_profile_ = VIDEO_CODEC_PROFILE_UNKNOWN;
gfx::Size active_coded_size_;
+ gfx::Size active_max_coded_size_;
+ int active_bit_depth_ = 0;
base::apple::ScopedCFTypeRef<CMFormatDescriptionRef> active_format_;
VideoToolboxDecompressionSessionMetadata session_metadata_;
diff --git a/media/gpu/mac/video_toolbox_av1_accelerator_unittest.cc b/media/gpu/mac/video_toolbox_av1_accelerator_unittest.cc
index 0679898..afef753 100644
--- a/media/gpu/mac/video_toolbox_av1_accelerator_unittest.cc
+++ b/media/gpu/mac/video_toolbox_av1_accelerator_unittest.cc
@@ -133,4 +133,70 @@
EXPECT_THAT(data2, ElementsAreArray(show_existing_frame_data));
}
+TEST_F(VideoToolboxAV1AcceleratorTest, InvalidAV1CBox) {
+ // Data without sequence header OBU will fail to yield valid av1C.
+ constexpr uint8_t invalid_data[] = {0x01, 0x02, 0x03, 0x04};
+
+ libgav1::ObuSequenceHeader sequence_header = {};
+ sequence_header.profile = libgav1::kProfile0;
+ sequence_header.color_config.bitdepth = 8;
+
+ const AV1ReferenceFrameVector ref_frames;
+ const libgav1::Vector<libgav1::TileBuffer> tile_buffers;
+
+ scoped_refptr<AV1Picture> pic = accelerator_->CreateAV1Picture(false);
+ pic->frame_header.width = 320;
+ pic->frame_header.height = 240;
+
+ accelerator_->SetStream(base::span(invalid_data), nullptr);
+ EXPECT_EQ(accelerator_->SubmitDecode(*pic, sequence_header, ref_frames,
+ tile_buffers, base::span(invalid_data)),
+ AV1Decoder::AV1Accelerator::Status::kFail);
+}
+
+TEST_F(VideoToolboxAV1AcceleratorTest,
+ BitDepthChangeTriggersFormatRegeneration) {
+ constexpr uint8_t frame_data[] = {0x0a, 0x0b, 0x00, 0x00, 0x00, 0x04, 0x3c,
+ 0xff, 0xbc, 0xfb, 0xf9, 0x80, 0x40};
+
+ const AV1ReferenceFrameVector ref_frames;
+ const libgav1::Vector<libgav1::TileBuffer> tile_buffers;
+
+ scoped_refptr<AV1Picture> pic = accelerator_->CreateAV1Picture(false);
+ pic->frame_header.width = 320;
+ pic->frame_header.height = 240;
+ pic->set_visible_rect(gfx::Rect(320, 240));
+
+ VideoToolboxDecompressionSessionMetadata metadata_8bit;
+ VideoToolboxDecompressionSessionMetadata metadata_10bit;
+
+ // First decode: 8-bit.
+ libgav1::ObuSequenceHeader sequence_header_8bit = {};
+ sequence_header_8bit.profile = libgav1::kProfile0;
+ sequence_header_8bit.color_config.bitdepth = 8;
+
+ EXPECT_CALL(*this, OnDecode(_, _, _)).WillOnce(SaveArg<1>(&metadata_8bit));
+ EXPECT_CALL(*this, OnOutput(_));
+ accelerator_->SetStream(base::span(frame_data), nullptr);
+ EXPECT_EQ(accelerator_->SubmitDecode(*pic, sequence_header_8bit, ref_frames,
+ tile_buffers, base::span(frame_data)),
+ AV1Decoder::AV1Accelerator::Status::kOk);
+ accelerator_->OutputPicture(*pic);
+ EXPECT_EQ(metadata_8bit.bit_depth, 8);
+
+ // Second decode: Bit depth changes to 10-bit with frame dimensions unchanged.
+ libgav1::ObuSequenceHeader sequence_header_10bit = {};
+ sequence_header_10bit.profile = libgav1::kProfile0;
+ sequence_header_10bit.color_config.bitdepth = 10;
+
+ EXPECT_CALL(*this, OnDecode(_, _, _)).WillOnce(SaveArg<1>(&metadata_10bit));
+ EXPECT_CALL(*this, OnOutput(_));
+ accelerator_->SetStream(base::span(frame_data), nullptr);
+ EXPECT_EQ(accelerator_->SubmitDecode(*pic, sequence_header_10bit, ref_frames,
+ tile_buffers, base::span(frame_data)),
+ AV1Decoder::AV1Accelerator::Status::kOk);
+ accelerator_->OutputPicture(*pic);
+ EXPECT_EQ(metadata_10bit.bit_depth, 10);
+}
+
} // namespace media
Regression Test / PoC
diff --git a/media/gpu/mac/video_toolbox_av1_accelerator_unittest.cc b/media/gpu/mac/video_toolbox_av1_accelerator_unittest.cc
index 0679898..afef753 100644
--- a/media/gpu/mac/video_toolbox_av1_accelerator_unittest.cc
+++ b/media/gpu/mac/video_toolbox_av1_accelerator_unittest.cc
@@ -133,4 +133,70 @@
EXPECT_THAT(data2, ElementsAreArray(show_existing_frame_data));
}
+TEST_F(VideoToolboxAV1AcceleratorTest, InvalidAV1CBox) {
+ // Data without sequence header OBU will fail to yield valid av1C.
+ constexpr uint8_t invalid_data[] = {0x01, 0x02, 0x03, 0x04};
+
+ libgav1::ObuSequenceHeader sequence_header = {};
+ sequence_header.profile = libgav1::kProfile0;
+ sequence_header.color_config.bitdepth = 8;
+
+ const AV1ReferenceFrameVector ref_frames;
+ const libgav1::Vector<libgav1::TileBuffer> tile_buffers;
+
+ scoped_refptr<AV1Picture> pic = accelerator_->CreateAV1Picture(false);
+ pic->frame_header.width = 320;
+ pic->frame_header.height = 240;
+
+ accelerator_->SetStream(base::span(invalid_data), nullptr);
+ EXPECT_EQ(accelerator_->SubmitDecode(*pic, sequence_header, ref_frames,
+ tile_buffers, base::span(invalid_data)),
+ AV1Decoder::AV1Accelerator::Status::kFail);
+}
+
+TEST_F(VideoToolboxAV1AcceleratorTest,
+ BitDepthChangeTriggersFormatRegeneration) {
+ constexpr uint8_t frame_data[] = {0x0a, 0x0b, 0x00, 0x00, 0x00, 0x04, 0x3c,
+ 0xff, 0xbc, 0xfb, 0xf9, 0x80, 0x40};
+
+ const AV1ReferenceFrameVector ref_frames;
+ const libgav1::Vector<libgav1::TileBuffer> tile_buffers;
+
+ scoped_refptr<AV1Picture> pic = accelerator_->CreateAV1Picture(false);
+ pic->frame_header.width = 320;
+ pic->frame_header.height = 240;
+ pic->set_visible_rect(gfx::Rect(320, 240));
+
+ VideoToolboxDecompressionSessionMetadata metadata_8bit;
+ VideoToolboxDecompressionSessionMetadata metadata_10bit;
+
+ // First decode: 8-bit.
+ libgav1::ObuSequenceHeader sequence_header_8bit = {};
+ sequence_header_8bit.profile = libgav1::kProfile0;
+ sequence_header_8bit.color_config.bitdepth = 8;
+
+ EXPECT_CALL(*this, OnDecode(_, _, _)).WillOnce(SaveArg<1>(&metadata_8bit));
+ EXPECT_CALL(*this, OnOutput(_));
+ accelerator_->SetStream(base::span(frame_data), nullptr);
+ EXPECT_EQ(accelerator_->SubmitDecode(*pic, sequence_header_8bit, ref_frames,
+ tile_buffers, base::span(frame_data)),
+ AV1Decoder::AV1Accelerator::Status::kOk);
+ accelerator_->OutputPicture(*pic);
+ EXPECT_EQ(metadata_8bit.bit_depth, 8);
+
+ // Second decode: Bit depth changes to 10-bit with frame dimensions unchanged.
+ libgav1::ObuSequenceHeader sequence_header_10bit = {};
+ sequence_header_10bit.profile = libgav1::kProfile0;
+ sequence_header_10bit.color_config.bitdepth = 10;
+
+ EXPECT_CALL(*this, OnDecode(_, _, _)).WillOnce(SaveArg<1>(&metadata_10bit));
+ EXPECT_CALL(*this, OnOutput(_));
+ accelerator_->SetStream(base::span(frame_data), nullptr);
+ EXPECT_EQ(accelerator_->SubmitDecode(*pic, sequence_header_10bit, ref_frames,
+ tile_buffers, base::span(frame_data)),
+ AV1Decoder::AV1Accelerator::Status::kOk);
+ accelerator_->OutputPicture(*pic);
+ EXPECT_EQ(metadata_10bit.bit_depth, 10);
+}
+
} // namespace media
Original Bug Report
Potential VT driver OOB via unvalidated AV1 av1C generation and flawed change-detection logic
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: The macOS VideoToolbox AV1 accelerator fails to validate the size of generated av1C atoms and omits bit depth/max frame dimensions from its format change-detection logic. An attacker can feed malicious streams to trigger a 0-byte configuration record or reuse a stale 8-bit session for 10-bit decoding. This can result in potential driver-side out-of-bounds memory corruption within the sandboxed GPU process.
Affected files:
media/gpu/mac/video_toolbox_av1_accelerator.ccmedia/gpu/mac/vt_config_util.mm
Estimated timestamp from git blame: 2023-11-14
1. Summary of the Issue (Meant for Human Triage)
A security logic vulnerability exists in the macOS AV1 hardware decoder path (VideoToolboxAV1Accelerator::ProcessFormat). An attacker-controlled server can deliver a maliciously crafted AV1 bitstream to force Apple’s VideoToolbox graphics driver into processing out-of-spec or inconsistent stream structures, potentially causing out-of-bounds memory operations within the sandboxed GPU process.
The vulnerability stems from two independent flaws in the format regeneration logic:
- Unchecked Codec Configuration Box (
av1C) Retrieval: Format regeneration does not validate the output oflibgav1::ObuParser::GetAV1CodecConfigurationBox(). If format regeneration is triggered on a frame lacking an in-band Sequence Header,libgav1returns anullptrwith a size of0. The accelerator blindly wraps this in aspan, creating a 0-byteav1Cextension atom that is passed to Apple CoreMedia’sVTDecompressionSessionCreate. This violates the AV1-ISOBMFF specification (requiring >= 4 bytes) and hands a 0-byte bounds-bearing configuration structure to the driver. - Insufficient Format Change-Detection Keys: The accelerator tracks format changes by checking only
{color_space, profile, per-frame coded_size}. It completely omits the stream’s bit depth and sequence-level maximum frame dimensions (max_frame_width,max_frame_height). If a stream transitions from 8-bit to 10-bit, or drastically increases its maximum sequence frame size while holding the per-frame size constant, the format regeneration block is skipped. A stale 8-bit configuration is then used to decode 10-bit inputs viaVTDecompressionSessionDecodeFrame, forcing the driver to decode into undersized/incorrectly formatted memory surfaces.
Note: These steps represent potential exploitation paths based on code analysis; our tooling agent does not run or execute code to empirically demonstrate the Apple VideoToolbox internal driver panic.
2. Proof-of-Concept & Detailed Execution Flow
The following outlines the potential steps an attacker could take using a webpage with a <video> tag or WebCodecs configured for hardware AV1 decode on macOS (M3+ Apple Silicon).
Case A: 0-byte av1C Driver Submission via Reference-Frame Scaling
- Temporal Unit 1 (TU1): The attacker serves a chunk containing
[OBU_SEQUENCE_HEADER max_frame=4096x4096]and[OBU_FRAME key_frame width=2048 height=2048].AV1Decoder::SetStream()copies this intotemporal_unit_data_.ProcessFormatexecutes, branches into format regeneration, and extracts a validav1Cvialibgav1::ObuParser::GetAV1CodecConfigurationBox(media/gpu/mac/video_toolbox_av1_accelerator.cc:204).active_coded_size_is saved as{2048, 2048}. A validVTDecompressionSessionis created.
- Temporal Unit 2 (TU2): The attacker serves a chunk containing no Sequence Header OBU, just an
[OBU_FRAME inter_frame frame_size_override_flag=1 width=1024 height=1024].AV1Decoder::DecodeInternalprocesses this without returningkConfigChange.ProcessFormatcomputes the new per-framecoded_sizeas{1024, 1024}.- At
video_toolbox_av1_accelerator.cc:197,coded_size != active_coded_size_, so the regeneration branch is entered.active_format_.reset()executes. libgav1::ObuParser::GetAV1CodecConfigurationBox()runs on the TU2 bytes. Finding no sequence header,libgav1returnskStatusBitstreamErrorand outputsnullptrand*av1c_size = 0(third_party/libgav1/src/src/obu_parser.cc:3017-3019).- At
video_toolbox_av1_accelerator.cc:206,av1c_spanis constructed with{nullptr, 0}. No null-check or size boundary validation is performed. CreateFormatExtensions(vt_config_util.mm:241) receives the span.[NSData dataWithBytes:nullptr length:0]creates a 0-byteNSDataobject for theav1Cextension atom.CMVideoFormatDescriptionCreatesuccessfully builds a malformed format description, which is passed toVTDecompressionSessionCreatein Apple’s driver, risking an OOB memory fetch during parser initialization.
Case B: Stale av1C Reuse via Missing Parameter Keys
- Temporal Unit 1 (TU1): The attacker serves
[OBU_SEQUENCE_HEADER bitdepth=8, max_frame=128x128]+[OBU_FRAME key_frame 128x128].ProcessFormatinitializes a format description for 8-bit AV1 video.session_metadata_.bit_depthis set to8.- A VideoToolbox session configured with an 8-bit output pixel format is initialized.
- Temporal Unit 2 (TU2): The attacker serves
[OBU_SEQUENCE_HEADER bitdepth=10, max_frame=8192x8192]+[OBU_FRAME frame_size_override_flag=1 width=128 height=128].AV1Decoder::DecodeInternalparses the new sequence header and registers the bitdepth change, safely returningAcceleratedVideoDecoder::kConfigChange(media/gpu/av1_decoder.cc:385).VideoToolboxVideoDecoder::DecodereceiveskConfigChange, issues acontinue, and resumes without resetting the accelerator.ProcessFormatis invoked. It checks if the parameters changed (video_toolbox_av1_accelerator.cc:197):if (color_space != active_color_space_ || profile != active_profile_ || coded_size != active_coded_size_)color_spaceis identical (bitdepth is not mapped intoVideoColorSpace).profileis identical.coded_size(the 128x128 per-frame size) is identical.- Because
bitdepthandmax_frameare structurally omitted from this condition, the block is skipped.active_format_is not reset. VideoToolboxDecompressionSessionManager::Processcompares theCMFormatDescriptionRefpointer (format != active_format_.get()). Since they match, it bypasses format evaluation.- The macOS VideoToolbox HW-accelerated driver (
VTDecompressionSessionDecodeFrame) begins decoding a 10-bit frame with an 8192x8192 sequence maximum into an 8-bit driver-allocated context, forcing an out-of-bounds driver memory write.
Suggested Fix
- Case A: Add a boundary check immediately after
GetAV1CodecConfigurationBox()inProcessFormatto ensureav1cis not null andav1c_size >= 4. If the stream lacks a sequence header (causingnullptr), the decoder should either fetch the cached header or cleanly abort decoding. - Case B: Expand the change-detection validation in
ProcessFormatto evaluatesequence_header.color_config.bitdepth != session_metadata_.bit_depthand include a comparison against the maximum sequence dimensions.
3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)
> Verbatim Critic Verdict: The vulnerability report is accurate and verified in the codebase. In media/gpu/mac/video_toolbox_av1_accelerator.cc, ProcessFormat fails to validate the return value of libgav1::ObuParser::GetAV1CodecConfigurationBox(). If a frame chunk lacking a sequence header triggers a format regeneration (e.g., due to a per-frame resolution change), it returns nullptr and size 0. This directly leads to a 0-byte av1C atom being passed to VTDecompressionSessionCreate, violating the AV1-ISOBMFF §2.3 spec (which requires >= 4 bytes). Furthermore, the change-detection key at line 197 omits bitdepth and max_frame, meaning an in-band sequence header change from 8-bit to 10-bit will not trigger a format regeneration, feeding a 10-bit stream to an 8-bit VTDecompressionSessionDecodeFrame.
>
> Severity Justification: High (S1).
> 1. Ceiling (S1): The vulnerability exists in media/gpu/mac/, which is macOS-specific and compile-time excluded on Android. Therefore, the GPU process is sandboxed on the target platform. Memory corruption in a sandboxed GPU process reachable from web content is capped at High (S1); the Android-unsandboxed Critical (S0) rationale does not apply.
> 2. Modifier (S2 -> S1): The original report conservatively rated this as Medium (S2) because the closed-source nature of Apple’s VideoToolbox prevented empirical demonstration of an OOB write. However, per the explicit instruction: ‘Bitstream field passed unvalidated to a HW-decode driver struct… Do NOT reject for no ASAN report — the corruption is driver-side. PoC = the bad value at the delegate copy step.’ Because the spec-invalid values observably reach the driver call (VTDecompressionSessionCreate and VTDecompressionSessionDecodeFrame), we assess it at the full S1 severity.
Codebase Investigator Verifications:
- Data Passthrough Validation: We checked
media/gpu/av1_decoder.cc:627-646. InAV1Decoder::DecodeAndOutputPicture,*decoder_buffer_is passed as the final argument (data) toaccelerator_->SubmitDecode(). The parameter isscoped_refptr<media::DecoderBuffer>, which resolves viastd::ranges::contiguous_rangemapping directly tobase::span<const uint8_t> data. The bytes passed are identical to the chunk delivered by the renderer. - kConfigChange Handling: We verified
media/gpu/mac/video_toolbox_video_decoder.cc:260-300.VideoToolboxVideoDecoder::Decodesafely identifiesAcceleratedVideoDecoder::kConfigChange(line 273), and subsequently triggers acontinue;(line 274). This loops without resetting or alteringaccelerator_, proving the premise of Case B. - Manager Pointer Comparison: We analyzed
VideoToolboxDecompressionSessionManager::Process(media/gpu/mac/video_toolbox_decompression_session_manager.mm:154-170). The logic explicitly gatesdecompression_session_->CanAcceptFormat(format)behind the pointer comparisonformat != active_format_.get(). If the format description reference matches, validation is bypassed.
Environmental & Feature Checks:
- The feature requires
VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1), natively available on Apple Silicon M3+ / A17 Pro+ hardware. - The vulnerability triggers purely through A-SERVER logic via the
<video>orWebCodecsAPIs. No user interaction or prior renderer compromise is required.
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.