High patch-now ✔︎ verified CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebRTC
DescriptionUse after free in WebRTC
ComponentWebRTC
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
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.
Independent review by AIConfirmed
Reviewer notes: Confirmed; commit states the race leaves dangling PacketRouter pointers after RtpVideoSender destruction. Exploitation requires winning a tight transport-queue timing race.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker500767595
Fix commit77d265670fa7 (src) +76/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedMozilla
Reported2026-04-09 (per vendor advisory)
Fix landed2026-04-10 (public git)
Disclosed2026-04-28
Patch gap18 days — fix public in source before the advisory

Background

`RtpVideoSender`
the WebRTC object that owns per-stream RTP/RTCP modules (rtp_streams_[i].rtp_rtcp) and registers/unregisters them with the shared PacketRouter.
`PacketRouter`
a transport-side registry of active RTP sender modules; a module registered there is a raw pointer that must be unregistered before the owning RtpVideoSender is destroyed.
Transport queue vs. encoder thread
module (de)registration must run on the transport sequence (transport_checker_), while OnVideoLayersAllocationUpdated runs on the encoder thread and posts work to the transport queue.
`SetSending(false)` / `active_`
deactivates the sender and unregisters its modules from PacketRouter; IsActive() reports the current active_ state.

Root Cause Analysis

OnVideoLayersAllocationUpdated runs on the encoder thread, observes that the sender is active, and posts a task (transport_queue_.PostTask(SafeTask(...))) to the transport queue to update module status by calling SetModuleIsActive(sending[i], *rtp_streams_[i].rtp_rtcp), which re-registers modules with the PacketRouter. Between the active-check and the posted task actually running, SetSending(false) can execute on the transport queue, deactivating the sender and unregistering its modules — a time-of-check/time-of-use gap. When the stale posted task then runs, it re-registers modules into PacketRouter even though active_ is now false; because subsequent teardown expects an inactive sender to have already been unregistered, the modules remain registered as dangling raw pointers in PacketRouter after the RtpVideoSender is destroyed.

The fix adds if (!IsActive()) { return; } at the top of the posted task (before RTC_CHECK_EQ(...) and the SetModuleIsActive loop), so a deactivation that won the race causes the task to bail out instead of re-registering, keeping PacketRouter consistent with the sender’s lifetime.

Key insight
The active-state check happened on the encoder thread before posting, but was never re-verified on the transport queue where deactivation could intervene; the fix re-checks IsActive() inside the posted task so a racing SetSending(false) prevents stale re-registration of modules that would otherwise dangle in PacketRouter.

Attack Path

  1. Post a re-registration task OnVideoLayersAllocationUpdated (per the commit message; enclosing function name not shown in the hunk) sees the sender active and posts a transport-queue task to re-register modules based on the new VideoLayersAllocation.
  2. Deactivate before the task runs SetSending(false) executes on the transport queue first, setting active_ false and unregistering the modules from PacketRouter.
  3. Stale task re-registers modules The previously posted task runs and, without an activity re-check, calls SetModuleIsActive(...) to re-register the modules into PacketRouter despite the sender being inactive.
  4. Dangling pointers on destruction The RtpVideoSender is destroyed while its modules remain registered in PacketRouter, leaving dangling raw pointers whose later use is a use-after-free (regression test asserts PacketRouter has no sender after the race).

Impact Assessment

An attacker who can drive a WebRTC send stream to emit a video-layers-allocation update and then rapidly stop sending can win this race to leave freed RTP modules registered in the shared PacketRouter, yielding a use-after-free of the sender modules within the renderer/WebRTC context. The advisory classifies this as a use-after-free in WebRTC and does not state in-the-wild exploitation. Any escalation beyond the dangling-pointer condition the patch prevents is not demonstrated and is (inferred).

Changed Functions

FunctionChangeNotes
TEST
call/rtp_video_sender_unittest.cc
modified

Files Changed

  • call/BUILD.gn
  • call/rtp_video_sender.cc
  • call/rtp_video_sender_unittest.cc

Audit Directions

  • Check-then-post across sequences
    Audit any handler that samples state (e.g. active_/IsActive()) on one thread and posts registration/teardown work to another; the posted task must re-validate that state before mutating shared registries.
  • Raw-pointer registries vs. object lifetime
    Review every place a module or object is registered into a shared table (PacketRouter and similar) to confirm unregistration is guaranteed on all deactivation/destruction orderings.
  • `SafeTask`/weak-flag guards
    Verify that a surviving SafeTask flag alone is not mistaken for a liveness/activeness guarantee; object still-alive does not imply still-active.
From 77d265670fa7d7f358c86c54704a6e97c27a6005 Mon Sep 17 00:00:00 2001
From: Harald Alvestrand <hta@webrtc.org>
Date: Fri, 10 Apr 2026 11:52:19 +0000
Subject: [PATCH] Fix TOCTOU race in RtpVideoSender::OnVideoLayersAllocationUpdated

RtpVideoSender::OnVideoLayersAllocationUpdated runs on the encoder thread,
checks if it's active, and then posts a task to the transport queue to
update module status.

If SetSending(false) is called on the transport queue before the posted
task runs, the task would previously re-register modules in PacketRouter
even though active_ was false. This lead to dangling pointers in
PacketRouter when the RtpVideoSender was destroyed.

This CL adds an IsActive() check inside the posted task to ensure we don't
re-register modules if the sender has been deactivated.

Bug: chromium:500767595
Test: RtpVideoSenderTest.PostTaskRaceDoesNotLeadToDanglingPointer
Change-Id: I18827c34bece7f5ebc41d159d9e19e356afbc404
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/462880
Commit-Queue: Harald Alvestrand <hta@webrtc.org>
Reviewed-by: Erik Språng <sprang@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#47384}
---

diff --git a/call/BUILD.gn b/call/BUILD.gn
index 815d28a..a784332 100644
--- a/call/BUILD.gn
+++ b/call/BUILD.gn
@@ -550,6 +550,7 @@
         "../api/video:video_codec_constants",
         "../api/video:video_frame",
         "../api/video:video_frame_type",
+        "../api/video:video_layers_allocation",
         "../api/video:video_rtp_headers",
         "../api/video_codecs:video_codecs_api",
         "../audio",
diff --git a/call/rtp_video_sender.cc b/call/rtp_video_sender.cc
index 73e8898..7267add 100644
--- a/call/rtp_video_sender.cc
+++ b/call/rtp_video_sender.cc
@@ -695,6 +695,11 @@
     transport_queue_.PostTask(
         SafeTask(safety_.flag(), [this, sending = std::move(sending)] {
           RTC_DCHECK_RUN_ON(&transport_checker_);
+          // It's possible for another task to be scheduled on the transport
+          // checker ahead of this call that makes the sender not active.
+          if (!IsActive()) {
+            return;
+          }
           RTC_CHECK_EQ(sending.size(), rtp_streams_.size());
           for (size_t i = 0; i < sending.size(); ++i) {
             SetModuleIsActive(sending[i], *rtp_streams_[i].rtp_rtcp);
diff --git a/call/rtp_video_sender_unittest.cc b/call/rtp_video_sender_unittest.cc
index a979e05..d7d30df 100644
--- a/call/rtp_video_sender_unittest.cc
+++ b/call/rtp_video_sender_unittest.cc
@@ -40,6 +40,7 @@
 #include "api/video/encoded_image.h"
 #include "api/video/video_codec_type.h"
 #include "api/video/video_frame_type.h"
+#include "api/video/video_layers_allocation.h"
 #include "api/video_codecs/video_encoder.h"
 #include "call/rtp_config.h"
 #include "call/rtp_transport_config.h"
@@ -1607,4 +1608,73 @@
   EXPECT_EQ(retransmitted_rtp_sequence_numbers, base_rtp_sequence_numbers);
 }
 
+TEST(RtpVideoSenderTest, PostTaskRaceDoesNotLeadToDanglingPointer) {
+  NiceMock<MockTransport> transport;
+  NiceMock<MockRtcpIntraFrameObserver> encoder_feedback;
+
+  GlobalSimulatedTimeController time_controller(Timestamp::Millis(1000000));
+  Environment env = CreateEnvironment(time_controller.GetClock(),
+                                      time_controller.CreateTaskQueueFactory());
+
+  VideoSendStream::Config config(&transport);
+  config.rtp.ssrcs = {kSsrc1};
+
+  SendStatisticsProxy stats_proxy(
+      time_controller.GetClock(), config,
+      VideoEncoderConfig::ContentType::kRealtimeVideo, env.field_trials());
+
+  BitrateConstraints bitrate_config = GetBitrateConfig();
+  RtpTransportConfig transport_config{.env = env,
+                                      .bitrate_config = bitrate_config};
+  RtpTransportControllerSend transport_controller(transport_config);
+  transport_controller.EnsureStarted();
+
+  RateLimiter retransmission_rate_limiter(time_controller.GetClock(),
+                                          kRetransmitWindowSizeMs);
+
+  std::map<uint32_t, RtpState> suspended_ssrcs;
+  std::map<uint32_t, RtpPayloadState> suspended_payload_states;
+
+  auto router = std::make_unique<RtpVideoSender>(
+      env, time_controller.GetMainThread(), suspended_ssrcs,
+      suspended_payload_states, config.rtp, config.rtcp_report_interval_ms,
+      &transport,
+      CreateObservers(&encoder_feedback, &stats_proxy, &stats_proxy,
+                      &stats_proxy, nullptr, &stats_proxy),
+      &transport_controller, &retransmission_rate_limiter,
+      std::make_unique<FecControllerDefault>(env), nullptr, CryptoOptions{},
+      nullptr);
+
+  router->SetSending(true);
+  // Verify it's registered
+  EXPECT_TRUE(
+      transport_controller.packet_router()->SsrcOfFirstSender().has_value());
+
+  // Trigger race:
+  // 1. OnVideoLayersAllocationUpdated posts a task to re-register modules
+  // because it sees active_ is true.
+  VideoLayersAllocation allocation;
+  allocation.active_spatial_layers.push_back({.rtp_stream_index = 0});
+  router->OnVideoLayersAllocationUpdated(allocation);
+
+  // 2. Immediately set active_ to false. This happens BEFORE the posted task
+  // runs.
+  router->SetSending(false);
+
+  // 3. Let the posted task run. With the fix, it should return early and not
+  // re-register the module because active_ is now false.
+  time_controller.AdvanceTime(TimeDelta::Zero());
+
+  // 4. Verify that PacketRouter does NOT have a sender.
+  EXPECT_FALSE(
+      transport_controller.packet_router()->SsrcOfFirstSender().has_value());
+
+  // 5. Destroy the router.
+  router.reset();
+
+  // 6. Verify that PacketRouter still does not have a sender.
+  EXPECT_FALSE(
+      transport_controller.packet_router()->SsrcOfFirstSender().has_value());
+}
+
 }  // namespace webrtc
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/call/rtp_video_sender_unittest.cc b/call/rtp_video_sender_unittest.cc
index a979e05..d7d30df 100644
--- a/call/rtp_video_sender_unittest.cc
+++ b/call/rtp_video_sender_unittest.cc
@@ -40,6 +40,7 @@
 #include "api/video/encoded_image.h"
 #include "api/video/video_codec_type.h"
 #include "api/video/video_frame_type.h"
+#include "api/video/video_layers_allocation.h"
 #include "api/video_codecs/video_encoder.h"
 #include "call/rtp_config.h"
 #include "call/rtp_transport_config.h"
@@ -1607,4 +1608,73 @@
   EXPECT_EQ(retransmitted_rtp_sequence_numbers, base_rtp_sequence_numbers);
 }
 
+TEST(RtpVideoSenderTest, PostTaskRaceDoesNotLeadToDanglingPointer) {
+  NiceMock<MockTransport> transport;
+  NiceMock<MockRtcpIntraFrameObserver> encoder_feedback;
+
+  GlobalSimulatedTimeController time_controller(Timestamp::Millis(1000000));
+  Environment env = CreateEnvironment(time_controller.GetClock(),
+                                      time_controller.CreateTaskQueueFactory());
+
+  VideoSendStream::Config config(&transport);
+  config.rtp.ssrcs = {kSsrc1};
+
+  SendStatisticsProxy stats_proxy(
+      time_controller.GetClock(), config,
+      VideoEncoderConfig::ContentType::kRealtimeVideo, env.field_trials());
+
+  BitrateConstraints bitrate_config = GetBitrateConfig();
+  RtpTransportConfig transport_config{.env = env,
+                                      .bitrate_config = bitrate_config};
+  RtpTransportControllerSend transport_controller(transport_config);
+  transport_controller.EnsureStarted();
+
+  RateLimiter retransmission_rate_limiter(time_controller.GetClock(),
+                                          kRetransmitWindowSizeMs);
+
+  std::map<uint32_t, RtpState> suspended_ssrcs;
+  std::map<uint32_t, RtpPayloadState> suspended_payload_states;
+
+  auto router = std::make_unique<RtpVideoSender>(
+      env, time_controller.GetMainThread(), suspended_ssrcs,
+      suspended_payload_states, config.rtp, config.rtcp_report_interval_ms,
+      &transport,
+      CreateObservers(&encoder_feedback, &stats_proxy, &stats_proxy,
+                      &stats_proxy, nullptr, &stats_proxy),
+      &transport_controller, &retransmission_rate_limiter,
+      std::make_unique<FecControllerDefault>(env), nullptr, CryptoOptions{},
+      nullptr);
+
+  router->SetSending(true);
+  // Verify it's registered
+  EXPECT_TRUE(
+      transport_controller.packet_router()->SsrcOfFirstSender().has_value());
+
+  // Trigger race:
+  // 1. OnVideoLayersAllocationUpdated posts a task to re-register modules
+  // because it sees active_ is true.
+  VideoLayersAllocation allocation;
+  allocation.active_spatial_layers.push_back({.rtp_stream_index = 0});
+  router->OnVideoLayersAllocationUpdated(allocation);
+
+  // 2. Immediately set active_ to false. This happens BEFORE the posted task
+  // runs.
+  router->SetSending(false);
+
+  // 3. Let the posted task run. With the fix, it should return early and not
+  // re-register the module because active_ is now false.
+  time_controller.AdvanceTime(TimeDelta::Zero());
+
+  // 4. Verify that PacketRouter does NOT have a sender.
+  EXPECT_FALSE(
+      transport_controller.packet_router()->SsrcOfFirstSender().has_value());
+
+  // 5. Destroy the router.
+  router.reset();
+
+  // 6. Verify that PacketRouter still does not have a sender.
+  EXPECT_FALSE(
+      transport_controller.packet_router()->SsrcOfFirstSender().has_value());
+}
+
 }  // namespace webrtc
Loading diff…

Original Bug Report

reported by ap...@mozilla.com

Heap-use-after-free in [@ webrtc::PacketRouter::SendCombinedRtcpPacket] via TOCTOU in RtpVideoSender::OnVideoLayersAllocationUpdated

Security Bug

As reported on the Firefox bugzilla.


RtpVideoSender::OnVideoLayersAllocationUpdated runs on the encoder thread, takes mutex_, checks IsActiveLocked(), and then posts a SafeTask to the transport queue that calls SetModuleIsActive(sending[i], rtp_streams_[i].rtp_rtcp) without re-validating active_. If SetSending(false) executes on the transport queue between the encoder’s check and the posted task’s execution, it sets active_=false and deregisters all RTP modules from PacketRouter. The stale SafeTask then runs and re-registers the raw ModuleRtpRtcpImpl2 via transport_->RegisterSendingRtpStream() while active_ is already false.

When the RtpVideoSender is subsequently destroyed (e.g. when the send stream is recreated after setting an RTCRtpScriptTransform), ~RtpVideoSender calls SetActiveModulesLocked(false), which early-returns at line 510 because active_ is already false. The re-registered module is therefore never removed from PacketRouter::send_modules_list_, and rtp_streams_ is destroyed, freeing the ModuleRtpRtcpImpl2. PacketRouter is owned by RtpTransportControllerSend and outlives the RtpVideoSender, so it now holds a dangling pointer.

The next periodic transport-cc feedback tick invokes PacketRouter::SendCombinedRtcpPacket, which iterates send_modules_list_ and performs a virtual call rtp_module->RTCP() on the freed object, reading the vtable from freed heap memory. The code path is reachable from unprivileged web content by SDP-munging the video-layers-allocation00 RTP header extension into the negotiated session (Firefox’s JSEP layer accepts the added extmap), using only canvas.captureStream() and standard RTCPeerConnection APIs with no permission prompt. The natural race window is narrow; a usleep(100000) inside the locked region was used to demonstrate it deterministically without altering program semantics.

Build Info

Affected Code

File: third_party/libwebrtc/call/rtp_video_sender.cc, line 669-700

void RtpVideoSender::OnVideoLayersAllocationUpdated(
    const VideoLayersAllocation& allocation) {
  MutexLock lock(&mutex_);
  if (IsActiveLocked()) {
    for (size_t i = 0; i < rtp_streams_.size(); ++i) {
      VideoLayersAllocation stream_allocation = allocation;
      stream_allocation.rtp_stream_index = i;
      rtp_streams_[i].sender_video->SetVideoLayersAllocation(
          std::move(stream_allocation));
    }
    ...
    transport_queue_.PostTask(
        SafeTask(safety_.flag(), [this, sending = std::move(sending)] {
          RTC_DCHECK_RUN_ON(&transport_checker_);
          RTC_CHECK_EQ(sending.size(), rtp_streams_.size());
          for (size_t i = 0; i < sending.size(); ++i) {
            SetModuleIsActive(sending[i], *rtp_streams_[i].rtp_rtcp);  // no re-check of active_
          }
        }));
  }
}

File: third_party/libwebrtc/call/rtp_video_sender.cc, line 494-523

RtpVideoSender::~RtpVideoSender() {
  RTC_DCHECK_RUN_ON(&transport_checker_);
  SetActiveModulesLocked(/*sending=*/false);
}

void RtpVideoSender::SetSending(bool enabled) {
  RTC_DCHECK_RUN_ON(&transport_checker_);
  MutexLock lock(&mutex_);
  if (enabled == active_) {
    return;
  }
  SetActiveModulesLocked(/*sending=*/enabled);
}

void RtpVideoSender::SetActiveModulesLocked(bool sending) {
  RTC_DCHECK_RUN_ON(&transport_checker_);
  if (active_ == sending) {
    return;                 // early-return: re-registered module is never removed
  }
  active_ = sending;
  for (const RtpStreamSender& stream : rtp_streams_) {
    SetModuleIsActive(sending, *stream.rtp_rtcp);
  }
  ...
}

File: third_party/libwebrtc/call/rtp_video_sender.cc, line 525-539

void RtpVideoSender::SetModuleIsActive(bool sending,
                                       RtpRtcpInterface& rtp_module) {
  if (rtp_module.SendingMedia() == sending) {
    return;
  }
  rtp_module.SetSendingStatus(sending);
  rtp_module.SetSendingMediaStatus(sending);
  if (sending) {
    transport_->RegisterSendingRtpStream(rtp_module);   // re-registers raw pointer in PacketRouter
  } else {
    transport_->DeRegisterSendingRtpStream(rtp_module);
  }
}

File: third_party/libwebrtc/modules/pacing/packet_router.cc, line 325-336

void PacketRouter::SendCombinedRtcpPacket(
    std::vector<std::unique_ptr<rtcp::RtcpPacket>> packets) {
  RTC_DCHECK_RUN_ON(&thread_checker_);

  // Prefer send modules.
  for (RtpRtcpInterface* rtp_module : send_modules_list_) {
    if (rtp_module->RTCP() == RtcpMode::kOff) {   // UAF: virtual call on freed ModuleRtpRtcpImpl2
      continue;
    }
    rtp_module->SendCombinedRtcpPacket(std::move(packets));
    return;
  }
  ...

The encoder-thread callback posts a transport-queue task that toggles module registration based on a stale snapshot of active_. If SetSending(false) wins the race, the destructor’s SetActiveModulesLocked(false) early-returns and the re-registered raw pointer is left dangling in PacketRouter::send_modules_list_, where it is later dereferenced by SendCombinedRtcpPacket.

Exploit Chain

  1. Attacker page creates two RTCPeerConnections and a canvas.captureStream() video track (no permission prompt).
  2. Page munges local offer/answer SDP to add ‘a=extmap:14 http://www.webrtc.org/experiments/rtp-hdrext/video-layers-allocation00' so the send stream uses BitrateAllocationCallbackType::kVideoLayersAllocation, causing OnVideoLayersAllocationUpdated to fire on the encoder thread.
  3. Page calls sender.setParameters() to force an encoder reconfigure while frames are flowing, generating OnVideoLayersAllocationUpdated callbacks that post SetModuleIsActive SafeTasks to the transport queue.
  4. Page changes transceiver direction to ‘recvonly’ and renegotiates: VideoSendStreamImpl::Stop -> RtpVideoSender::SetSending(false) runs on the transport queue. If this acquires mutex_ while the encoder thread is between IsActiveLocked() and PostTask, the SafeTask is queued after SetSending(false) and re-registers the ModuleRtpRtcpImpl2 while active_==false.
  5. Page sets sender.transform = new RTCRtpScriptTransform(…), forcing WebrtcVideoConduit::OnControlConfigChange -> DeleteSendStream -> Call::DestroyVideoSendStream -> ~RtpVideoSender. SetActiveModulesLocked(false) early-returns (active_ already false), the module is not deregistered, and rtp_streams_ frees the ModuleRtpRtcpImpl2.
  6. The peer continues sending media; ReceiveSideCongestionController’s periodic task calls PacketRouter::SendCombinedRtcpPacket, which performs a virtual call on the dangling ModuleRtpRtcpImpl2*.
  7. Attacker grooms the freed 1456-byte slot with a fake vtable pointer to hijack control flow in the content process.

Steps to Reproduce

  1. Apply the race-widening patch to third_party/libwebrtc/call/rtp_video_sender.cc (add ‘#include <unistd.h>’ and ‘usleep(100000);’ immediately after ‘if (IsActiveLocked()) {’ in OnVideoLayersAllocationUpdated). This only widens the existing race window; it does not change logic.
  2. Rebuild Firefox: ./mach build
  3. Set pref media.navigator.permission.disabled=true (convenience only; the testcase uses canvas.captureStream which needs no permission).
  4. Load the provided test.html in an ASAN build.
  5. Within ~10 seconds the content process aborts with AddressSanitizer: heap-use-after-free in webrtc::PacketRouter::SendCombinedRtcpPacket.

Security Impact

  • Severity: High
  • Attacker capability: Heap use-after-free with an attacker-influenceable freed allocation (1456-byte ModuleRtpRtcpImpl2). The first dereference is a vtable load followed by an indirect virtual call (rtp_module->RTCP()), giving a control-flow-hijack primitive in the content process if the freed slot is reclaimed with attacker data. Reachable from any web origin via standard WebRTC APIs and SDP munging with no user interaction or permission prompt.
  • Preconditions: User visits an attacker-controlled web page. No non-default prefs are required. The natural race window is on the order of microseconds, so practical exploitation requires winning a tight thread race (demonstrated deterministically here with a 100ms sleep that does not change semantics).

ASAN Report

==24608==ERROR: AddressSanitizer: heap-use-after-free on address 0x78bed7e8fe80 at pc 0x770eb6b80948 bp 0x770e847cf7d0 sp 0x770e847cf7c8
READ of size 8 at 0x78bed7e8fe80 thread T26
    #0 0x770eb6b80947 in webrtc::PacketRouter::SendCombinedRtcpPacket(std::vector<std::unique_ptr<webrtc::rtcp::RtcpPacket, std::default_delete<webrtc::rtcp::RtcpPacket>>, std::allocator<std::unique_ptr<webrtc::rtcp::RtcpPacket, std::default_delete<webrtc::rtcp::RtcpPacket>>>>) /firefox/third_party/libwebrtc/modules/pacing/packet_router.cc:331:21
    #3 0x770eb6b9c99e in webrtc::TransportSequenceNumberFeedbackGenenerator::SendPeriodicFeedbacks() /firefox/third_party/libwebrtc/modules/remote_bitrate_estimator/transport_sequence_number_feedback_generator.cc:189:5
    #4 0x770eb6b9c5da in webrtc::TransportSequenceNumberFeedbackGenenerator::Process(webrtc::Timestamp) /firefox/third_party/libwebrtc/modules/remote_bitrate_estimator/transport_sequence_number_feedback_generator.cc:136:5
    #5 0x770eb6b14a67 in webrtc::ReceiveSideCongestionController::MaybeProcess() /firefox/third_party/libwebrtc/modules/congestion_controller/receive_side_congestion_controller.cc
    #14 0x770eb65eaf08 in mozilla::InvocableRunnable::Run() /firefox/dom/media/webrtc/libwebrtcglue/WebrtcTaskQueueWrapper.cpp:41:5

0x78bed7e8fe80 is located 0 bytes inside of 1456-byte region [0x78bed7e8fe80,0x78bed7e90430)
freed by thread T26 here:
    #4 0x770eb688cb0a in webrtc::webrtc_internal_rtp_video_sender::RtpStreamSender::~RtpStreamSender() /firefox/third_party/libwebrtc/call/rtp_video_sender.cc:87:35
    #11 0x770eb68837a5 in webrtc::RtpVideoSender::~RtpVideoSender() /firefox/third_party/libwebrtc/call/rtp_video_sender.cc:498:1
    #18 0x770eb6f7f679 in webrtc::internal::VideoSendStreamImpl::~VideoSendStreamImpl() /firefox/third_party/libwebrtc/video/video_send_stream_impl.cc:527:15
    #20 0x770eb683da3a in webrtc::internal::Call::DestroyVideoSendStream(webrtc::VideoSendStream*) /firefox/third_party/libwebrtc/call/call.cc:976:3
    #21 0x770eb659c939 in mozilla::WebrtcVideoConduit::DeleteSendStream() /firefox/dom/media/webrtc/libwebrtcglue/VideoConduit.cpp:1034:18
    #22 0x770eb659c939 in mozilla::WebrtcVideoConduit::OnControlConfigChange() /firefox/dom/media/webrtc/libwebrtcglue/VideoConduit.cpp:957:7

previously allocated by thread T26 here:
    #4 0x770eb6880987 in webrtc::(anonymous namespace)::CreateRtpStreamSenders(...) /firefox/third_party/libwebrtc/call/rtp_video_sender.cc:296:21
    #5 0x770eb6880987 in webrtc::RtpVideoSender::RtpVideoSender(...) /firefox/third_party/libwebrtc/call/rtp_video_sender.cc:409:20
    #7 0x770eb6876303 in webrtc::RtpTransportControllerSend::CreateRtpVideoSender(...) /firefox/third_party/libwebrtc/call/rtp_transport_controller_send.cc:169:32
    #8 0x770eb6f7d328 in webrtc::internal::VideoSendStreamImpl::VideoSendStreamImpl(...) /firefox/third_party/libwebrtc/video/video_send_stream_impl.cc:429:36
    #11 0x770eb65a15b3 in mozilla::WebrtcVideoConduit::CreateSendStream() /firefox/dom/media/webrtc/libwebrtcglue/VideoConduit.cpp:1077:32

SUMMARY: AddressSanitizer: heap-use-after-free (/firefox/obj-firefox-asan/dist/bin/libxul.so+0x1d6a6947)

VULNERABILITY DETAILS

UAF in libwebrtc that requires a sender with the video-layers-allocation00 RTP header extension (appears possible at will), and the ability to recreate a video send stream (appears possible at will).

VERSION

Chrome Version: 123 and forward. Appears to be a regression from 979b6d62 (the early return guard in SetActiveModulesLocked), so all current release branches. Operating System: platform independent

REPRODUCTION CASE

Happy to give someone access to the Bugzilla bug on request. I can submit a patch once I’ve sorted it locally in Firefox.

FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION

Type of crash: UAF in a content/tab process Crash State: Client ID (if relevant):

CREDIT INFORMATION

Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited? Reporter credit: Mozilla

View on issue tracker