Medium patch-now CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in WebRTC
DescriptionOut of bounds write in WebRTC
ComponentWebRTC
Bug ClassOOB
Action tierpatch-now — corruption primitive, web-reachable in default config (unverified)
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker496266456
Fix commit6e3bbc294b05 (chromium/src) +348/-2
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedsoft.connect.fr
Reported2026-03-26 (per vendor advisory)
Fix landed2026-03-27 (public git)
Disclosed2026-05-05
Patch gap39 days — fix public in source before the advisory

Background

`RTCRtpSender.setParameters`
the WebRTC JS API (implemented in Blink’s rtc_rtp_sender.cc) that applies caller-supplied send parameters, including per-encoding options, to an RTP sender.
`encodings` vs `encoding_options`
encodings is the sender’s actual encoding list (from getParameters); encoding_options is the caller-supplied RTCSetParameterOptions list, which is required to have the same size.
Promise rejection
on an invalid argument the method rejects the returned promise with a DOMException (here kInvalidModificationError) and is expected to stop.
`request_key_frame` mapping
after validation the method copies each encoding_options[i]->keyFrame() into encodings[i].request_key_frame.

Root Cause Analysis

The vulnerable code is the size-mismatch branch of RTCRtpSender::setParameters in third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc (the enclosing function name is not shown in the @@ -859 hunk header but is identified by the commit subject and the added rtc_rtp_sender_test.cc). When encoding_options.size() did not match the number of encodings, the code called resolver->RejectWithDOMException(..., "encodingOptions size must match number of encodings.") but then fell through instead of returning. The violated invariant is that encoding_options.size() == encodings.size() must hold before the subsequent loop for (i = 0; i < encoding_options.size(); i++) encodings[i].request_key_frame = encoding_options[i]->keyFrame(); indexes encodings by i. With more options than encodings, the loop indexes encodings[i] past its bounds — an out-of-bounds write — despite the promise already having been rejected.

The fix adds return promise; immediately after the rejection, so the mismatched state is never processed and the loop never runs with an out-of-range index.

Key insight
The size-mismatch error path rejected the promise but omitted the early return, so execution continued into a loop that indexed encodings by the (larger) encoding_options count; adding return promise; prevents the out-of-bounds write.

Attack Path

  1. Obtain a sender and its parameters Web content creates an RTCPeerConnection/RTCRtpSender and calls getParameters() to get an encodings list of some length N.
  2. Call `setParameters` with mismatched options The page calls setParameters supplying an RTCSetParameterOptions whose encodingOptions has more entries than N encodings.
  3. Rejection without return Pre-patch, setParameters rejects the promise for the size mismatch but does not return, so control reaches the request_key_frame mapping loop.
  4. Out-of-bounds write The loop iterates up to encoding_options.size() and writes encodings[i].request_key_frame for i >= N, writing past the end of the encodings container in the renderer process, the out-of-bounds write the advisory describes.

Impact Assessment

Web-page JavaScript could trigger an out-of-bounds write in the renderer process by calling setParameters with an encodingOptions list larger than the sender’s encodings; the advisory rates this medium and classifies it as an out-of-bounds write in WebRTC. The precondition is only the ability to run script that uses the WebRTC RTCRtpSender API (no special permissions). The advisory does not report in-the-wild exploitation, and the patch demonstrates an OOB write but not controlled memory corruption or code execution (inferred).

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

Audit Directions

  • Reject-without-return error paths
    Audit Blink promise/exception handlers that reject or throw but continue executing; a missing return after an argument-validation failure can let invalid state reach a sink.
  • Parallel-list index assumptions
    Look for loops that index one container by another container’s size after a size-equality check, and verify the failure branch truly aborts before the loop.
  • Web-exposed parameter mapping
    Review other setParameters-style APIs that copy caller-supplied option arrays into internal fixed-size vectors for the same size-mismatch handling.
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