CVE-2026-11667
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forvideo/encoder_bitrate_adjuster.cc |
modified | |
ifvideo/encoder_bitrate_adjuster.cc |
modified | |
TEST_Pvideo/encoder_bitrate_adjuster_unittest.cc |
modified | |
forvideo/encoder_bitrate_adjuster_unittest.cc |
modified |
Files Changed
video/encoder_bitrate_adjuster.ccvideo/encoder_bitrate_adjuster_unittest.cc
Patch
From 1ad94cdd43c06cbf1cfcdec665b83671fd287a5b Mon Sep 17 00:00:00 2001
From: Erik Språng <sprang@webrtc.org>
Date: Fri, 22 May 2026 13:07:00 +0200
Subject: [PATCH] Add better checks for temporal/spatial bounds in EncoderBitrateAdjuster.
Bug: webrtc:514671098
Change-Id: If3cfb55bdc03d57760a5d30fc63ca63ed321fad1
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/474681
Commit-Queue: Erik Språng <sprang@webrtc.org>
Reviewed-by: Philip Eliasson <philipel@webrtc.org>
Auto-Submit: Erik Språng <sprang@webrtc.org>
Commit-Queue: Philip Eliasson <philipel@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#47775}
---
diff --git a/video/encoder_bitrate_adjuster.cc b/video/encoder_bitrate_adjuster.cc
index 10c7334..d8aa226 100644
--- a/video/encoder_bitrate_adjuster.cc
+++ b/video/encoder_bitrate_adjuster.cc
@@ -76,9 +76,16 @@
if (codec_settings.codecType == VideoCodecType::kVideoCodecAV1 &&
codec_settings.numberOfSimulcastStreams <= 1 &&
codec_settings.GetScalabilityMode().has_value()) {
- for (int si = 0; si < ScalabilityModeToNumSpatialLayers(
- *(codec_settings.GetScalabilityMode()));
- ++si) {
+ const int num_spatial_layers = ScalabilityModeToNumSpatialLayers(
+ *(codec_settings.GetScalabilityMode()));
+ for (int si = 0; si < num_spatial_layers; ++si) {
+ if (si >= static_cast<int>(kMaxSpatialLayers)) {
+ RTC_LOG(LS_WARNING)
+ << "AV1 scalability mode specifies " << num_spatial_layers
+ << " spatial layers, which exceeds kMaxSpatialLayers ("
+ << kMaxSpatialLayers << ")";
+ break;
+ }
if (codec_settings.spatialLayers[si].active) {
min_bitrates_bps_[si] =
std::max(codec_settings.minBitrate * 1000,
@@ -88,6 +95,13 @@
} else if (codec_settings.codecType == VideoCodecType::kVideoCodecVP9 &&
codec_settings.numberOfSimulcastStreams <= 1) {
for (size_t si = 0; si < codec_settings.VP9().numberOfSpatialLayers; ++si) {
+ if (si >= kMaxSpatialLayers) {
+ RTC_LOG(LS_WARNING)
+ << "VP9 specifies " << codec_settings.VP9().numberOfSpatialLayers
+ << " spatial layers, which exceeds kMaxSpatialLayers ("
+ << kMaxSpatialLayers << ")";
+ break;
+ }
if (codec_settings.spatialLayers[si].active) {
min_bitrates_bps_[si] =
std::max(codec_settings.minBitrate * 1000,
@@ -96,6 +110,13 @@
}
} else {
for (size_t si = 0; si < codec_settings.numberOfSimulcastStreams; ++si) {
+ if (si >= kMaxSpatialLayers) {
+ RTC_LOG(LS_WARNING)
+ << "Codec specifies " << codec_settings.numberOfSimulcastStreams
+ << " simulcast streams, which exceeds kMaxSpatialLayers ("
+ << kMaxSpatialLayers << ")";
+ break;
+ }
if (codec_settings.simulcastStream[si].active) {
min_bitrates_bps_[si] =
std::max(codec_settings.minBitrate * 1000,
@@ -372,6 +393,12 @@
// Copy allocation into current state and re-allocate.
for (size_t si = 0; si < kMaxSpatialLayers; ++si) {
current_fps_allocation_[si] = encoder_info.fps_allocation[si];
+ if (current_fps_allocation_[si].size() > kMaxTemporalStreams) {
+ RTC_LOG(LS_WARNING) << "fps_allocation has more than "
+ << kMaxTemporalStreams
+ << " temporal streams. Truncating.";
+ current_fps_allocation_[si].resize(kMaxTemporalStreams);
+ }
}
// Trigger re-allocation so that overshoot detectors have correct targets.
@@ -381,6 +408,15 @@
void EncoderBitrateAdjuster::OnEncodedFrame(DataSize size,
int stream_index,
int temporal_index) {
+ if (stream_index < 0 || stream_index >= static_cast<int>(kMaxSpatialLayers) ||
+ temporal_index < 0 ||
+ temporal_index >= static_cast<int>(kMaxTemporalStreams)) {
+ RTC_LOG(LS_WARNING) << "OnEncodedFrame called with invalid layer: "
+ << "stream_index = " << stream_index
+ << ", temporal_index = " << temporal_index;
+ return;
+ }
+
++frames_since_layout_change_;
// Detectors may not exist, for instance if ScreenshareLayers is used.
auto& detector = overshoot_detectors_[stream_index][temporal_index];
diff --git a/video/encoder_bitrate_adjuster_unittest.cc b/video/encoder_bitrate_adjuster_unittest.cc
index 7514405..14bb11e 100644
--- a/video/encoder_bitrate_adjuster_unittest.cc
+++ b/video/encoder_bitrate_adjuster_unittest.cc
@@ -581,6 +581,40 @@
ExpectNear(expected_input_allocation, current_adjusted_allocation_, 0.01);
}
+TEST_P(EncoderBitrateAdjusterTest, OnEncodedFrameInvalidLayers) {
+ current_input_allocation_.SetBitrate(0, 0, 300000);
+ target_framerate_fps_ = 30;
+ SetUpAdjuster(1, 1, false);
+
+ // Call OnEncodedFrame with invalid stream indices and make sure it doesn't
+ // crash.
+ adjuster_->OnEncodedFrame(DataSize::Bytes(1000), -1, 0);
+ adjuster_->OnEncodedFrame(DataSize::Bytes(1000), kMaxSpatialLayers, 0);
+
+ // Call OnEncodedFrame with invalid temporal indices and make sure it doesn't
+ // crash.
+ adjuster_->OnEncodedFrame(DataSize::Bytes(1000), 0, -1);
+ adjuster_->OnEncodedFrame(DataSize::Bytes(1000), 0, kMaxTemporalStreams);
+}
+
+TEST_P(EncoderBitrateAdjusterTest,
+ OnEncoderInfoTruncatesTooManyTemporalStreams) {
+ current_input_allocation_.SetBitrate(0, 0, 300000);
+ target_framerate_fps_ = 30;
+ SetUpAdjuster(1, 1, false);
+
+ // Create an EncoderInfo with a temporal allocation larger than
+ // kMaxTemporalStreams.
+ VideoEncoder::EncoderInfo encoder_info;
+ encoder_info.fps_allocation[0].resize(kMaxTemporalStreams + 2);
+ for (size_t ti = 0; ti < kMaxTemporalStreams + 2; ++ti) {
+ encoder_info.fps_allocation[0][ti] = 255;
+ }
+
+ // This should truncate to kMaxTemporalStreams and not crash.
+ adjuster_->OnEncoderInfo(encoder_info);
+}
+
INSTANTIATE_TEST_SUITE_P(
AdjustWithHeadroomVariations,
EncoderBitrateAdjusterTest,
Regression Test / PoC
diff --git a/video/encoder_bitrate_adjuster_unittest.cc b/video/encoder_bitrate_adjuster_unittest.cc
index 7514405..14bb11e 100644
--- a/video/encoder_bitrate_adjuster_unittest.cc
+++ b/video/encoder_bitrate_adjuster_unittest.cc
@@ -581,6 +581,40 @@
ExpectNear(expected_input_allocation, current_adjusted_allocation_, 0.01);
}
+TEST_P(EncoderBitrateAdjusterTest, OnEncodedFrameInvalidLayers) {
+ current_input_allocation_.SetBitrate(0, 0, 300000);
+ target_framerate_fps_ = 30;
+ SetUpAdjuster(1, 1, false);
+
+ // Call OnEncodedFrame with invalid stream indices and make sure it doesn't
+ // crash.
+ adjuster_->OnEncodedFrame(DataSize::Bytes(1000), -1, 0);
+ adjuster_->OnEncodedFrame(DataSize::Bytes(1000), kMaxSpatialLayers, 0);
+
+ // Call OnEncodedFrame with invalid temporal indices and make sure it doesn't
+ // crash.
+ adjuster_->OnEncodedFrame(DataSize::Bytes(1000), 0, -1);
+ adjuster_->OnEncodedFrame(DataSize::Bytes(1000), 0, kMaxTemporalStreams);
+}
+
+TEST_P(EncoderBitrateAdjusterTest,
+ OnEncoderInfoTruncatesTooManyTemporalStreams) {
+ current_input_allocation_.SetBitrate(0, 0, 300000);
+ target_framerate_fps_ = 30;
+ SetUpAdjuster(1, 1, false);
+
+ // Create an EncoderInfo with a temporal allocation larger than
+ // kMaxTemporalStreams.
+ VideoEncoder::EncoderInfo encoder_info;
+ encoder_info.fps_allocation[0].resize(kMaxTemporalStreams + 2);
+ for (size_t ti = 0; ti < kMaxTemporalStreams + 2; ++ti) {
+ encoder_info.fps_allocation[0][ti] = 255;
+ }
+
+ // This should truncate to kMaxTemporalStreams and not crash.
+ adjuster_->OnEncoderInfo(encoder_info);
+}
+
INSTANTIATE_TEST_SUITE_P(
AdjustWithHeadroomVariations,
EncoderBitrateAdjusterTest,
Original Bug Report
OOB array access in EncoderBitrateAdjuster via unvalidated Mojo temporal_idx
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 compromised GPU process can trigger an out-of-bounds array access in the renderer process by providing an unvalidated temporal index in Mojo video encoding metadata. This unvalidated index is used by the WebRTC component to access a fixed-size array, leading to potential memory corruption or type confusion. The vulnerability is reachable when hardware-accelerated video encoding is active.
Affected files:
third_party/webrtc/video/encoder_bitrate_adjuster.ccthird_party/webrtc/video/encoder_bitrate_adjuster.hmedia/mojo/mojom/video_encode_accelerator_mojom_traits.ccthird_party/blink/renderer/platform/peerconnection/rtc_video_encoder.ccthird_party/webrtc/video/video_stream_encoder.cc
Estimated timestamp from git blame: 2019-02-07
Summary
A potential out-of-bounds (OOB) array access vulnerability exists in webrtc::EncoderBitrateAdjuster::OnEncodedFrame. The issue stems from a lack of validation for the temporal_idx field in Mojo messages sent from the GPU process to the renderer process. A compromised GPU process can exploit this to cause memory corruption in the sandboxed renderer process.
Root Cause Analysis
The temporal_idx originates in the GPU process and is passed to the renderer via the media.mojom.VideoEncodeAcceleratorClient.BitstreamBufferReady Mojo IPC. The BitstreamBufferMetadata structure contains codec-specific metadata (e.g., H264Metadata, Vp8Metadata, or Vp9Metadata) that includes a temporal_idx field.
In media/mojo/mojom/video_encode_accelerator_mojom_traits.cc, the Mojo StructTraits deserialize this value without range validation. Subsequently, in third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.cc, RTCVideoEncoder::Impl::BitstreamBufferReady extracts this index and stores it in a webrtc::EncodedImage without validation. While webrtc::EncodedImage has internal RTC_DCHECK protections, these are inactive in production release builds.
The unvalidated index eventually reaches webrtc::EncoderBitrateAdjuster::OnEncodedFrame in third_party/webrtc/video/encoder_bitrate_adjuster.cc:
void EncoderBitrateAdjuster::OnEncodedFrame(DataSize size,
int stream_index,
int temporal_index) {
// ...
auto& detector = overshoot_detectors_[stream_index][temporal_index]; // OOB access if temporal_index >= 4
if (detector) {
detector->OnEncodedFrame(size.bytes(), clock_.TimeInMicroseconds() / 1000);
}
// ...
}
The overshoot_detectors_ array is a fixed-size 2D array defined in encoder_bitrate_adjuster.h as [kMaxSpatialLayers][kMaxTemporalStreams], where kMaxSpatialLayers is 5 and kMaxTemporalStreams is 4. An attacker providing a temporal_idx greater than 3 will cause an out-of-bounds access.
Potential Impact
An out-of-bounds temporal_index (e.g., 20) can cause the code to alias adjacent members in EncoderBitrateAdjuster. Specifically, it may alias the media_rate_trackers_ array, resulting in internal type confusion where a RateUtilizationTracker object is treated as an EncoderOvershootDetector. The subsequent call to OnEncodedFrame on this mismatched object can lead to heap corruption via corrupted std::deque operations.
Potential Steps to Reproduce
- From a compromised GPU process, respond to a renderer’s
Encoderequest with aBitstreamBufferReadymessage. - In the
BitstreamBufferMetadata, set a codec-specifictemporal_idx(e.g., inH264Metadata) to a value outside the range [0, 3]. - Ensure the renderer is using hardware acceleration for the corresponding codec (e.g., via a WebRTC connection).
- When the renderer receives the metadata, it will eventually trigger the OOB access in
EncoderBitrateAdjuster.
Suggested Fix
Apply range validation to the temporal_idx (and spatial_idx where applicable) within RTCVideoEncoder::Impl::BitstreamBufferReady before passing the metadata to WebRTC. Additionally, consider adding validation to the Mojo StructTraits in media/mojo/mojom/video_encode_accelerator_mojom_traits.cc to ensure received indices are within the limits defined by webrtc::kMaxSpatialLayers and webrtc::kMaxTemporalStreams.
Evaluated with Chrome root at commit: 29093e11cf509e3593f6229e4b1b075cca356049
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.