Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in WebRTC
DescriptionOut of bounds read in WebRTC
ComponentWebRTC
Bug ClassOOB
Tracker514671098
Fix commit1ad94cdd43c0 (src) +73/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-08

Changed Functions

FunctionChangeNotes
for
video/encoder_bitrate_adjuster.cc
modified
if
video/encoder_bitrate_adjuster.cc
modified
TEST_P
video/encoder_bitrate_adjuster_unittest.cc
modified
for
video/encoder_bitrate_adjuster_unittest.cc
modified

Files Changed

  • video/encoder_bitrate_adjuster.cc
  • video/encoder_bitrate_adjuster_unittest.cc
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,
Loading diff…

Regression Test / PoC

shipped with the fix
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,
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • third_party/webrtc/video/encoder_bitrate_adjuster.h
  • media/mojo/mojom/video_encode_accelerator_mojom_traits.cc
  • third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.cc
  • third_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

  1. From a compromised GPU process, respond to a renderer’s Encode request with a BitstreamBufferReady message.
  2. In the BitstreamBufferMetadata, set a codec-specific temporal_idx (e.g., in H264Metadata) to a value outside the range [0, 3].
  3. Ensure the renderer is using hardware acceleration for the corresponding codec (e.g., via a WebRTC connection).
  4. 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.

View on issue tracker