Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in WebRTC
DescriptionOut of bounds write in WebRTC
ComponentWebRTC
Bug ClassOOB
Tracker496266456
Fix commit6e3bbc294b05 (chromium/src) +348/-2
CISA KEVNot listed
Creditedsoft.connect.fr
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
RTCRtpSender
third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.h
modified
MODULES_EXPORT
third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.h
modified
FakeRTCRtpSenderPlatform
third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_test.cc
modified
EncodingsFakeRTCRtpSenderPlatform
third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_test.cc
modified
RejectingFakeRTCRtpSenderPlatform
third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_test.cc
modified

Files Changed

  • third_party/blink/renderer/modules/BUILD.gn
  • third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc
  • third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.h
  • third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_test.cc
From 6e3bbc294b0529fbe90e6026e198b72ad37d763d Mon Sep 17 00:00:00 2001
From: Johannes Kron <[email protected]>
Date: Fri, 27 Mar 2026 04:59:28 -0700
Subject: [PATCH] Fix early return in RTCRtpSender::setParameters

Ensure the function returns immediately after rejecting the promise due
to an encoding options size mismatch, preventing further processing of
invalid state.

Fixed: 496266456
Change-Id: I8e09b973336751dd9c0a320e313f2e910acb2ad7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7705898
Auto-Submit: Johannes Kron <[email protected]>
Commit-Queue: Johannes Kron <[email protected]>
Reviewed-by: Harald Alvestrand <[email protected]>
Commit-Queue: Harald Alvestrand <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1606116}
---

diff --git a/third_party/blink/renderer/modules/BUILD.gn b/third_party/blink/renderer/modules/BUILD.gn
index 7e0589b..591296db 100644
--- a/third_party/blink/renderer/modules/BUILD.gn
+++ b/third_party/blink/renderer/modules/BUILD.gn
@@ -583,6 +583,7 @@
     "peerconnection/rtc_rtp_receiver_impl_test.cc",
     "peerconnection/rtc_rtp_script_transformer_test.cc",
     "peerconnection/rtc_rtp_sender_impl_test.cc",
+    "peerconnection/rtc_rtp_sender_test.cc",
     "peerconnection/rtc_rtp_transceiver_impl_test.cc",
     "peerconnection/rtc_sctp_transport_test.cc",
     "peerconnection/rtc_transport/rtc_received_packet_test.cc",
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc
index 90d271f..6d7519e 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc
@@ -859,6 +859,7 @@
       resolver->RejectWithDOMException(
           DOMExceptionCode::kInvalidModificationError,
           "encodingOptions size must match number of encodings.");
+      return promise;
     }
     for (wtf_size_t i = 0; i < encoding_options.size(); i++) {
       encodings[i].request_key_frame = encoding_options[i]->keyFrame();
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.h b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.h
index cb7bcc8..abe6ae7 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.h
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.h
@@ -15,6 +15,7 @@
 #include "third_party/blink/renderer/bindings/modules/v8/v8_rtc_set_parameter_options.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context_lifecycle_observer.h"
 #include "third_party/blink/renderer/modules/mediastream/media_stream.h"
+#include "third_party/blink/renderer/modules/modules_export.h"
 #include "third_party/blink/renderer/modules/peerconnection/rtc_rtp_script_transform.h"
 #include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
@@ -54,8 +55,9 @@
     const webrtc::RtpCodecParameters& codecs);
 
 // https://w3c.github.io/webrtc-pc/#rtcrtpsender-interface
-class RTCRtpSender final : public ScriptWrappable,
-                           public ExecutionContextLifecycleObserver {
+class MODULES_EXPORT RTCRtpSender final
+    : public ScriptWrappable,
+      public ExecutionContextLifecycleObserver {
   DEFINE_WRAPPERTYPEINFO();
 
  public:
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_test.cc b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_test.cc
new file mode 100644
index 0000000..3a34a921
--- /dev/null
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_test.cc
@@ -0,0 +1,342 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.h"
+
+#include <memory>
+#include <optional>
+
+#include "base/run_loop.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/platform/scheduler/test/renderer_scheduler_test_support.h"
+#include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_rtc_configuration.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_rtc_encoding_options.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_rtc_rtp_encoding_parameters.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_rtc_rtp_send_parameters.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_rtc_set_parameter_options.h"
+#include "third_party/blink/renderer/modules/peerconnection/mock_rtc_peer_connection_handler_platform.h"
+#include "third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.h"
+#include "third_party/blink/renderer/platform/peerconnection/rtc_encoded_audio_stream_transformer.h"
+#include "third_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer.h"
+#include "third_party/blink/renderer/platform/peerconnection/rtc_rtp_sender_platform.h"
+#include "third_party/blink/renderer/platform/peerconnection/rtc_void_request.h"
+#include "third_party/blink/renderer/platform/testing/task_environment.h"
+#include "third_party/webrtc/api/rtc_error.h"
+#include "third_party/webrtc/api/rtp_parameters.h"
+
+namespace blink {
+
+namespace {
+
+class FakeRTCRtpSenderPlatform : public RTCRtpSenderPlatform {
+ public:
+  FakeRTCRtpSenderPlatform()
+      : audio_transformer_(std::make_unique<RTCEncodedAudioStreamTransformer>(
+            blink::scheduler::GetSingleThreadTaskRunnerForTesting())),
+        video_transformer_(std::make_unique<RTCEncodedVideoStreamTransformer>(
+            blink::scheduler::GetSingleThreadTaskRunnerForTesting(),
+            /*metronome=*/nullptr)) {}
+  ~FakeRTCRtpSenderPlatform() override = default;
+
+  std::unique_ptr<RTCRtpSenderPlatform> ShallowCopy() const override {
+    return nullptr;
+  }
+
+  uintptr_t Id() const override { return 1; }
+  webrtc::scoped_refptr<webrtc::DtlsTransportInterface> DtlsTransport()
+      override {
+    return nullptr;
+  }
+  webrtc::DtlsTransportInformation DtlsTransportInformation() override {
+    return webrtc::DtlsTransportInformation(webrtc::DtlsTransportState::kNew);
+  }
+  MediaStreamComponent* Track() const override { return nullptr; }
+  Vector<String> StreamIds() const override { return {}; }
+  void ReplaceTrack(MediaStreamComponent*, RTCVoidRequest*) override {}
+  std::unique_ptr<RtcDtmfSenderHandler> GetDtmfSender() const override {
+    return nullptr;
+  }
+
+  std::unique_ptr<webrtc::RtpParameters> GetParameters() const override {
+    auto parameters = std::make_unique<webrtc::RtpParameters>();
+    parameters->transaction_id = "1234";
+    return parameters;
+  }
+
+  void SetParameters(Vector<webrtc::RtpEncodingParameters>,
+                     std::optional<webrtc::DegradationPreference>,
+                     RTCVoidRequest* request) override {
+    set_parameters_called_ = true;
+    request->RequestSucceeded();
+  }
+
+  void GetStats(RTCStatsReportCallback) override {}
+  void SetStreams(const Vector<String>& stream_ids) override {}
+  RTCEncodedAudioStreamTransformer* GetEncodedAudioStreamTransformer()
+      const override {
+    return audio_transformer_.get();
+  }
+  RTCEncodedVideoStreamTransformer* GetEncodedVideoStreamTransformer()
+      const override {
+    return video_transformer_.get();
+  }
+
+  bool set_parameters_called_ = false;
+
+ private:
+  std::unique_ptr<RTCEncodedAudioStreamTransformer> audio_transformer_;
+  std::unique_ptr<RTCEncodedVideoStreamTransformer> video_transformer_;
+};
+
+// Simulates a native layer that successfully returns an active encoding.
+// It also tracks the encodings passed to SetParameters to verify mutations.
+class EncodingsFakeRTCRtpSenderPlatform : public FakeRTCRtpSenderPlatform {
+ public:
+  std::unique_ptr<webrtc::RtpParameters> GetParameters() const override {
+    auto parameters = std::make_unique<webrtc::RtpParameters>();
+    parameters->transaction_id = "1234";
+    // Add one encoding to test size matching and key_frame mapping.
+    parameters->encodings.emplace_back();
+    return parameters;
+  }
+
+  void SetParameters(
+      Vector<webrtc::RtpEncodingParameters> encodings,
+      std::optional<webrtc::DegradationPreference> degradation_preference,
+      RTCVoidRequest* request) override {
+    set_parameters_called_ = true;
+    last_encodings_ = encodings;
+    request->RequestSucceeded();
+  }
+
+  Vector<webrtc::RtpEncodingParameters> last_encodings_;
+};
+
+// Simulates a failure at the lower WebRTC native layer.
+class RejectingFakeRTCRtpSenderPlatform : public FakeRTCRtpSenderPlatform {
+ public:
+  void SetParameters(Vector<webrtc::RtpEncodingParameters>,
+                     std::optional<webrtc::DegradationPreference>,
+                     RTCVoidRequest* request) override {
+    set_parameters_called_ = true;
+    request->RequestFailed(webrtc::RTCError(
+        webrtc::RTCErrorType::UNSUPPORTED_PARAMETER, "Native layer rejected"));
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_test.cc b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_test.cc
new file mode 100644
index 0000000..3a34a921
--- /dev/null
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_test.cc
@@ -0,0 +1,342 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.h"
+
+#include <memory>
+#include <optional>
+
+#include "base/run_loop.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/platform/scheduler/test/renderer_scheduler_test_support.h"
+#include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_rtc_configuration.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_rtc_encoding_options.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_rtc_rtp_encoding_parameters.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_rtc_rtp_send_parameters.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_rtc_set_parameter_options.h"
+#include "third_party/blink/renderer/modules/peerconnection/mock_rtc_peer_connection_handler_platform.h"
+#include "third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.h"
+#include "third_party/blink/renderer/platform/peerconnection/rtc_encoded_audio_stream_transformer.h"
+#include "third_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer.h"
+#include "third_party/blink/renderer/platform/peerconnection/rtc_rtp_sender_platform.h"
+#include "third_party/blink/renderer/platform/peerconnection/rtc_void_request.h"
+#include "third_party/blink/renderer/platform/testing/task_environment.h"
+#include "third_party/webrtc/api/rtc_error.h"
+#include "third_party/webrtc/api/rtp_parameters.h"
+
+namespace blink {
+
+namespace {
+
+class FakeRTCRtpSenderPlatform : public RTCRtpSenderPlatform {
+ public:
+  FakeRTCRtpSenderPlatform()
+      : audio_transformer_(std::make_unique<RTCEncodedAudioStreamTransformer>(
+            blink::scheduler::GetSingleThreadTaskRunnerForTesting())),
+        video_transformer_(std::make_unique<RTCEncodedVideoStreamTransformer>(
+            blink::scheduler::GetSingleThreadTaskRunnerForTesting(),
+            /*metronome=*/nullptr)) {}
+  ~FakeRTCRtpSenderPlatform() override = default;
+
+  std::unique_ptr<RTCRtpSenderPlatform> ShallowCopy() const override {
+    return nullptr;
+  }
+
+  uintptr_t Id() const override { return 1; }
+  webrtc::scoped_refptr<webrtc::DtlsTransportInterface> DtlsTransport()
+      override {
+    return nullptr;
+  }
+  webrtc::DtlsTransportInformation DtlsTransportInformation() override {
+    return webrtc::DtlsTransportInformation(webrtc::DtlsTransportState::kNew);
+  }
+  MediaStreamComponent* Track() const override { return nullptr; }
+  Vector<String> StreamIds() const override { return {}; }
+  void ReplaceTrack(MediaStreamComponent*, RTCVoidRequest*) override {}
+  std::unique_ptr<RtcDtmfSenderHandler> GetDtmfSender() const override {
+    return nullptr;
+  }
+
+  std::unique_ptr<webrtc::RtpParameters> GetParameters() const override {
+    auto parameters = std::make_unique<webrtc::RtpParameters>();
+    parameters->transaction_id = "1234";
+    return parameters;
+  }
+
+  void SetParameters(Vector<webrtc::RtpEncodingParameters>,
+                     std::optional<webrtc::DegradationPreference>,
+                     RTCVoidRequest* request) override {
+    set_parameters_called_ = true;
+    request->RequestSucceeded();
+  }
+
+  void GetStats(RTCStatsReportCallback) override {}
+  void SetStreams(const Vector<String>& stream_ids) override {}
+  RTCEncodedAudioStreamTransformer* GetEncodedAudioStreamTransformer()
+      const override {
+    return audio_transformer_.get();
+  }
+  RTCEncodedVideoStreamTransformer* GetEncodedVideoStreamTransformer()
+      const override {
+    return video_transformer_.get();
+  }
+
+  bool set_parameters_called_ = false;
+
+ private:
+  std::unique_ptr<RTCEncodedAudioStreamTransformer> audio_transformer_;
+  std::unique_ptr<RTCEncodedVideoStreamTransformer> video_transformer_;
+};
+
+// Simulates a native layer that successfully returns an active encoding.
+// It also tracks the encodings passed to SetParameters to verify mutations.
+class EncodingsFakeRTCRtpSenderPlatform : public FakeRTCRtpSenderPlatform {
+ public:
+  std::unique_ptr<webrtc::RtpParameters> GetParameters() const override {
+    auto parameters = std::make_unique<webrtc::RtpParameters>();
+    parameters->transaction_id = "1234";
+    // Add one encoding to test size matching and key_frame mapping.
+    parameters->encodings.emplace_back();
+    return parameters;
+  }
+
+  void SetParameters(
+      Vector<webrtc::RtpEncodingParameters> encodings,
+      std::optional<webrtc::DegradationPreference> degradation_preference,
+      RTCVoidRequest* request) override {
+    set_parameters_called_ = true;
+    last_encodings_ = encodings;
+    request->RequestSucceeded();
+  }
+
+  Vector<webrtc::RtpEncodingParameters> last_encodings_;
+};
+
+// Simulates a failure at the lower WebRTC native layer.
+class RejectingFakeRTCRtpSenderPlatform : public FakeRTCRtpSenderPlatform {
+ public:
+  void SetParameters(Vector<webrtc::RtpEncodingParameters>,
+                     std::optional<webrtc::DegradationPreference>,
+                     RTCVoidRequest* request) override {
+    set_parameters_called_ = true;
+    request->RequestFailed(webrtc::RTCError(
+        webrtc::RTCErrorType::UNSUPPORTED_PARAMETER, "Native layer rejected"));
+  }
+};
+
+class RTCRtpSenderTest : public testing::Test {
+ public:
+  RTCRtpSenderTest() = default;
+
+  RTCPeerConnection* CreatePC(V8TestingScope& scope) {
+    RTCPeerConnection::SetRtcPeerConnectionHandlerFactoryForTesting(
+        base::BindRepeating([]() -> std::unique_ptr<RTCPeerConnectionHandler> {
+          return std::make_unique<MockRTCPeerConnectionHandlerPlatform>();
+        }));
+    return RTCPeerConnection::Create(scope.GetExecutionContext(),
+                                     RTCConfiguration::Create(),
+                                     scope.GetExceptionState());
+  }
+
+  RTCRtpSender* CreateSender(
+      RTCPeerConnection* pc,
+      std::unique_ptr<RTCRtpSenderPlatform> platform = nullptr) {
+    if (!platform) {
+      platform = std::make_unique<FakeRTCRtpSenderPlatform>();
+    }
+    return MakeGarbageCollected<RTCRtpSender>(
+        pc, std::move(platform), "video", nullptr, MediaStreamVector(), false,
+        blink::scheduler::GetSingleThreadTaskRunnerForTesting());
+  }
+
+ protected:
+  test::TaskEnvironment task_environment_;
+};
+
+}  // namespace
+
+TEST_F(RTCRtpSenderTest, SetParametersSucceeds) {
+  V8TestingScope scope;
+  RTCPeerConnection* pc = CreatePC(scope);
+  ASSERT_TRUE(pc);
+
+  auto platform = std::make_unique<FakeRTCRtpSenderPlatform>();
+  auto* platform_ptr = platform.get();
+  auto* sender = CreateSender(pc, std::move(platform));
+
+  auto* parameters = sender->getParameters();
+
+  auto* options = RTCSetParameterOptions::Create();
+  auto promise = sender->setParameters(scope.GetScriptState(), parameters,
+                                       options, scope.GetExceptionState());
+
+  ScriptPromiseTester tester(scope.GetScriptState(), promise);
+  tester.WaitUntilSettled();
+  EXPECT_TRUE(tester.IsFulfilled());
+
+  // Verify native layer was called.
+  EXPECT_TRUE(platform_ptr->set_parameters_called_);
+}
+
+TEST_F(RTCRtpSenderTest, SetParametersWithoutGetParametersFails) {
+  V8TestingScope scope;
+  RTCPeerConnection* pc = CreatePC(scope);
+  ASSERT_TRUE(pc);
+
+  auto platform = std::make_unique<FakeRTCRtpSenderPlatform>();
+  auto* platform_ptr = platform.get();
+  auto* sender = CreateSender(pc, std::move(platform));
+
+  auto* parameters = RTCRtpSendParameters::Create();
+  auto* options = RTCSetParameterOptions::Create();
+  auto promise = sender->setParameters(scope.GetScriptState(), parameters,
+                                       options, scope.GetExceptionState());
+
+  ScriptPromiseTester tester(scope.GetScriptState(), promise);
+  tester.WaitUntilSettled();
+  EXPECT_TRUE(tester.IsRejected());
+
+  // Verify native layer was never reached.
+  EXPECT_FALSE(platform_ptr->set_parameters_called_);
+
+  DOMException* exception =
+      V8DOMException::ToWrappable(scope.GetIsolate(), tester.Value().V8Value());
+  ASSERT_TRUE(exception);
+  EXPECT_EQ(exception->name(), "InvalidStateError");
+}
+
+TEST_F(RTCRtpSenderTest, SetParametersWithModifiedTransactionIdFails) {
+  V8TestingScope scope;
+  RTCPeerConnection* pc = CreatePC(scope);
+  ASSERT_TRUE(pc);
+
+  auto platform = std::make_unique<FakeRTCRtpSenderPlatform>();
+  auto* platform_ptr = platform.get();
+  auto* sender = CreateSender(pc, std::move(platform));
+
+  auto* parameters = sender->getParameters();
+  // Call getParameters() again to overwrite last_returned_parameters_,
+  // so that the modified `parameters` won't be identical to the saved state.
+  sender->getParameters();
+
+  parameters->setTransactionId("modified");
+
+  auto* options = RTCSetParameterOptions::Create();
+  auto promise = sender->setParameters(scope.GetScriptState(), parameters,
+                                       options, scope.GetExceptionState());
+
+  ScriptPromiseTester tester(scope.GetScriptState(), promise);
+  tester.WaitUntilSettled();
+  EXPECT_TRUE(tester.IsRejected());
+
+  // Verify native layer was never reached.
+  EXPECT_FALSE(platform_ptr->set_parameters_called_);
+
+  DOMException* exception =
+      V8DOMException::ToWrappable(scope.GetIsolate(), tester.Value().V8Value());
+  ASSERT_TRUE(exception);
+  EXPECT_EQ(exception->name(), "InvalidModificationError");
+}
+
+TEST_F(RTCRtpSenderTest, SetParametersWithMismatchedEncodingOptionsFails) {
+  V8TestingScope scope;
+  RTCPeerConnection* pc = CreatePC(scope);
+  ASSERT_TRUE(pc);
+
+  auto platform = std::make_unique<EncodingsFakeRTCRtpSenderPlatform>();
+  auto* platform_ptr = platform.get();
+  auto* sender = CreateSender(pc, std::move(platform));
+
+  auto* parameters = sender->getParameters();
+
+  // Provide 2 encoding options to force a mismatch with the 1 encoding.
+  auto* options = RTCSetParameterOptions::Create();
+  HeapVector<Member<RTCEncodingOptions>> encoding_options;
+  encoding_options.push_back(RTCEncodingOptions::Create());
+  encoding_options.push_back(RTCEncodingOptions::Create());
+  options->setEncodingOptions(encoding_options);
+
+  auto promise = sender->setParameters(scope.GetScriptState(), parameters,
+                                       options, scope.GetExceptionState());
+
+  ScriptPromiseTester tester(scope.GetScriptState(), promise);
+  tester.WaitUntilSettled();
+  EXPECT_TRUE(tester.IsRejected());
+
+  // Verify native layer was never reached.
+  EXPECT_FALSE(platform_ptr->set_parameters_called_);
+
+  DOMException* exception =
+      V8DOMException::ToWrappable(scope.GetIsolate(), tester.Value().V8Value());
+  ASSERT_TRUE(exception);
+  EXPECT_EQ(exception->name(), "InvalidModificationError");
+}
+
+TEST_F(RTCRtpSenderTest, SetParametersAppliesEncodingOptionsKeyFrame) {
+  V8TestingScope scope;
+  RTCPeerConnection* pc = CreatePC(scope);
+  ASSERT_TRUE(pc);
+
+  auto platform = std::make_unique<EncodingsFakeRTCRtpSenderPlatform>();
+  auto* platform_ptr = platform.get();
+  auto* sender = CreateSender(pc, std::move(platform));
+
+  auto* parameters = sender->getParameters();
+
+  // Provide 1 encoding option to match the 1 encoding, setting keyFrame to
+  // true.
+  auto* options = RTCSetParameterOptions::Create();
+  HeapVector<Member<RTCEncodingOptions>> encoding_options;
+  auto* encoding_option = RTCEncodingOptions::Create();
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Heap Out-of-Bounds Write in RTCRtpSender::setParameters

A missing return after RejectWithDOMException in RTCRtpSender::setParameters() allows a heap out-of-bounds write when encodingOptions.size() > encodings.size(). The loop at line 863 is bounded by encoding_options.size() but writes into encodings[], which can be smaller or empty.

Affected Component

Blink > WebRTC > RTCRtpSender

Heap out-of-bounds write reachable from any web page via the standard WebRTC JavaScript API. No user interaction required beyond visiting the page.

Bug Details

File: third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc Function: RTCRtpSender::setParameters() Lines: 858–865

// Line 855-866:
const auto& encoding_options = options->encodingOptions();
if (!encoding_options.empty()) {
  if (encoding_options.size() != encodings.size()) {
    resolver->RejectWithDOMException(
        DOMExceptionCode::kInvalidModificationError,
        "encodingOptions size must match number of encodings.");
    // BUG: Missing return here — execution falls through
  }
  for (wtf_size_t i = 0; i < encoding_options.size(); i++) {
    encodings[i].request_key_frame = encoding_options[i]->keyFrame();
    // ^^^ OOB write when encoding_options.size() > encodings.size()
  }
}

The size mismatch is detected at line 858 and an exception is thrown at line 859, but the code does NOT return. The for-loop at line 863 then iterates encoding_options.size() times, writing into encodings[i]. When encoding_options has more elements than encodings, this writes past the end of the Vector<webrtc::RtpEncodingParameters> heap buffer.

Proof of Concept

const pc = new RTCPeerConnection();
const stream = await navigator.mediaDevices.getUserMedia({audio: true});
const sender = pc.addTrack(stream.getAudioTracks()[0]);
const params = sender.getParameters();

// Set encodings to empty (0 elements)
params.encodings = [];

// Call setParameters with 3 encoding options but 0 encodings
// This triggers the OOB write: loop writes encodings[0], [1], [2]
// past the end of the empty encodings vector
try {
  await sender.setParameters(params, {
    encodingOptions: [
      {keyFrame: true},
      {keyFrame: true},
      {keyFrame: true}
    ]
  });
} catch(e) {
  // Exception is thrown but OOB write already happened
}

Impact

  1. Heap OOB write: encodings[i].request_key_frame = ... writes a bool at offset i * sizeof(webrtc::RtpEncodingParameters) past the end of the encodings vector. webrtc::RtpEncodingParameters is a large struct (~200+ bytes), so each iteration corrupts a significant chunk of adjacent heap memory.

  2. Attacker controls iteration count: The number of OOB writes equals encoding_options.size(), which is fully controlled by the attacker’s JavaScript.

  3. Corrupted data used downstream: After the OOB writes, the corrupted encodings vector is passed to sender_->SetParameters() at line 869, which forwards it to the WebRTC native layer. The corrupted struct fields may cause further memory corruption or unexpected behavior in the WebRTC stack.

  4. No special permissions required: The WebRTC API (RTCPeerConnection, addTrack, setParameters) is available to any web page. getUserMedia requires user permission for the initial track, but addTrack with any MediaStreamTrack (including from createMediaStreamDestination() in WebAudio, which requires no permission) is sufficient.

Root Cause

Copy-paste error: the error handling at line 858-861 follows the pattern of other checks in the same function (e.g., line 827 HasInvalidModification check), but those other checks all have return promise; after the rejection. This one was missed.

Suggested Fix

Add return promise; after the RejectWithDOMException call:

if (encoding_options.size() != encodings.size()) {
  resolver->RejectWithDOMException(
      DOMExceptionCode::kInvalidModificationError,
      "encodingOptions size must match number of encodings.");
  return promise;  // <-- ADD THIS
}

Version

Tested on Chromium 147.0.7721.0 (current ToT).

View on issue tracker