Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Cast
DescriptionUse after free in Cast
ComponentCast
Bug ClassUAF
Tracker522436154
Fix commit35415ca0da11 (chromium/src) +112/-100
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-16

Changed Functions

FunctionChangeNotes
if
components/cast_streaming/renderer/frame/demuxer_connector.cc
modified
FrameInjectingDemuxer
components/cast_streaming/renderer/frame/demuxer_connector.h
modified

Files Changed

  • components/cast_streaming/renderer/frame/demuxer_connector.cc
  • components/cast_streaming/renderer/frame/demuxer_connector.h
From 35415ca0da1163d2bfbff35565627fb7470956a0 Mon Sep 17 00:00:00 2001
From: Jordan Bayles <jophba@chromium.org>
Date: Thu, 09 Jul 2026 17:15:02 -0700
Subject: [PATCH] [cast_streaming] Fix UaF in FrameInjectingDemuxer with intermediate obj

Decouple DemuxerConnector (main renderer thread) and
FrameInjectingDemuxer (media thread) to simplify the demuxer
initialization sequence and resolve potential lifetime issues.

- Introduce DemuxerStreamConfigBuffer (base::RefCountedThreadSafe)
  to store stream initialization configurations received over Mojo IPC
  on the main thread.
- Remove raw demuxer pointer storage (demuxer_) and cross-thread
  registration/cleanup methods (SetDemuxer, OnDemuxerDestroyed) from
  DemuxerConnector.
- FrameInjectingDemuxer::Initialize reads stream configurations
  directly from DemuxerStreamConfigBuffer on the media thread without
  posting registration tasks across thread boundaries.
- Update ResourceProviderImpl to pass DemuxerConnector's config buffer
  to FrameInjectingDemuxer upon instantiation.

Fixed: 522436154
Change-Id: I3fa1dbe88db3f7719b260fdd60a0c87cac68fcd5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7981482
Reviewed-by: Simeon Anfinrud <sanfin@chromium.org>
Commit-Queue: Jordan Bayles <jophba@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1659974}
---

diff --git a/components/cast_streaming/renderer/frame/demuxer_connector.cc b/components/cast_streaming/renderer/frame/demuxer_connector.cc
index 547e5ef..74b0571 100644
--- a/components/cast_streaming/renderer/frame/demuxer_connector.cc
+++ b/components/cast_streaming/renderer/frame/demuxer_connector.cc
@@ -8,54 +8,68 @@
 
 namespace cast_streaming {
 
+DemuxerStreamConfigBuffer::DemuxerStreamConfigBuffer() = default;
+DemuxerStreamConfigBuffer::~DemuxerStreamConfigBuffer() = default;
+
+void DemuxerStreamConfigBuffer::SetConfigs(
+    mojom::AudioStreamInitializationInfoPtr audio_stream_info,
+    mojom::VideoStreamInitializationInfoPtr video_stream_info) {
+  base::OnceClosure closure;
+  scoped_refptr<base::SequencedTaskRunner> target_runner;
+  {
+    base::AutoLock lock(lock_);
+    if (has_configs_) {
+      return;
+    }
+    has_configs_ = true;
+    audio_stream_info_ = std::move(audio_stream_info);
+    video_stream_info_ = std::move(video_stream_info);
+
+    if (pending_callback_ && media_task_runner_) {
+      target_runner = media_task_runner_;
+      closure = base::BindOnce(std::move(pending_callback_),
+                               std::move(audio_stream_info_),
+                               std::move(video_stream_info_));
+    }
+  }
+  if (closure && target_runner) {
+    target_runner->PostTask(FROM_HERE, std::move(closure));
+  }
+}
+
+void DemuxerStreamConfigBuffer::ReadConfigs(
+    scoped_refptr<base::SequencedTaskRunner> media_task_runner,
+    ConfigCallback callback) {
+  base::OnceClosure closure;
+  {
+    base::AutoLock lock(lock_);
+    if (has_configs_) {
+      closure =
+          base::BindOnce(std::move(callback), std::move(audio_stream_info_),
+                         std::move(video_stream_info_));
+    } else {
+      media_task_runner_ = media_task_runner;
+      pending_callback_ = std::move(callback);
+    }
+  }
+  if (closure) {
+    media_task_runner->PostTask(FROM_HERE, std::move(closure));
+  }
+}
+
 DemuxerConnector::DemuxerConnector() = default;
 
 DemuxerConnector::~DemuxerConnector() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 }
 
-void DemuxerConnector::SetDemuxer(FrameInjectingDemuxer* demuxer) {
-  DVLOG(1) << __func__;
-  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  DCHECK(demuxer);
-
-  if (demuxer_) {
-    // We do not support more than one active FrameInjectingDemuxer in the same
-    // RenderFrame. Return early here.
-    demuxer->OnStreamsInitialized(mojom::AudioStreamInitializationInfoPtr(),
-                                  mojom::VideoStreamInitializationInfoPtr());
-    return;
-  }
-
-  DCHECK(!is_demuxer_initialized_);
-
-  if (IsBound()) {
-    demuxer_ = demuxer;
-    MaybeCallEnableReceiverCallback();
-  } else {
-    // The Cast Streaming Sender disconnected after |demuxer| was instantiated
-    // but before |demuxer| was initialized on the media thread.
-    demuxer->OnStreamsInitialized(mojom::AudioStreamInitializationInfoPtr(),
-                                  mojom::VideoStreamInitializationInfoPtr());
-  }
-}
-
-void DemuxerConnector::OnDemuxerDestroyed() {
-  DVLOG(1) << __func__;
-  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  DCHECK(demuxer_);
-
-  demuxer_ = nullptr;
-  is_demuxer_initialized_ = false;
-  demuxer_connector_receiver_.reset();
-}
-
 void DemuxerConnector::BindReceiver(
     mojo::PendingAssociatedReceiver<mojom::DemuxerConnector> receiver) {
   DVLOG(1) << __func__;
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   DCHECK(!demuxer_connector_receiver_.is_bound());
 
+  config_buffer_ = base::MakeRefCounted<DemuxerStreamConfigBuffer>();
   demuxer_connector_receiver_.Bind(std::move(receiver));
 
   // Mojo service disconnection means the Cast Streaming Session ended or the
@@ -75,7 +89,7 @@
   DVLOG(2) << __func__;
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
-  if (enable_receiver_callback_ && demuxer_) {
+  if (enable_receiver_callback_) {
     std::move(enable_receiver_callback_).Run();
   }
 }
@@ -87,10 +101,9 @@
   demuxer_connector_receiver_.reset();
   enable_receiver_callback_.Reset();
 
-  if (demuxer_ && !is_demuxer_initialized_) {
-    OnStreamsInitialized(mojom::AudioStreamInitializationInfoPtr(),
-                         mojom::VideoStreamInitializationInfoPtr());
-  }
+  config_buffer_->SetConfigs(mojom::AudioStreamInitializationInfoPtr(),
+                             mojom::VideoStreamInitializationInfoPtr());
+  config_buffer_ = base::MakeRefCounted<DemuxerStreamConfigBuffer>();
 }
 
 void DemuxerConnector::EnableReceiver(EnableReceiverCallback callback) {
@@ -108,12 +121,9 @@
     mojom::VideoStreamInitializationInfoPtr video_stream_info) {
   DVLOG(1) << __func__;
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  DCHECK(!is_demuxer_initialized_);
-  DCHECK(demuxer_);
 
-  is_demuxer_initialized_ = true;
-  demuxer_->OnStreamsInitialized(std::move(audio_stream_info),
-                                 std::move(video_stream_info));
+  config_buffer_->SetConfigs(std::move(audio_stream_info),
+                             std::move(video_stream_info));
 }
 
 }  // namespace cast_streaming
diff --git a/components/cast_streaming/renderer/frame/demuxer_connector.h b/components/cast_streaming/renderer/frame/demuxer_connector.h
index ad0e86f..5d21473 100644
--- a/components/cast_streaming/renderer/frame/demuxer_connector.h
+++ b/components/cast_streaming/renderer/frame/demuxer_connector.h
@@ -6,23 +6,53 @@
 #define COMPONENTS_CAST_STREAMING_RENDERER_FRAME_DEMUXER_CONNECTOR_H_
 
 #include "base/functional/callback.h"
-#include "base/memory/raw_ptr.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_refptr.h"
 #include "base/sequence_checker.h"
+#include "base/synchronization/lock.h"
+#include "base/task/sequenced_task_runner.h"
 #include "components/cast_streaming/common/public/mojom/demuxer_connector.mojom.h"
 #include "mojo/public/cpp/bindings/associated_receiver.h"
 #include "mojo/public/cpp/bindings/pending_associated_receiver.h"
 
 namespace cast_streaming {
 
-class FrameInjectingDemuxer;
+// Thread-safe buffer used to store stream initialization configurations
+// received over Mojo IPC on the main thread and dispatch them to
+// FrameInjectingDemuxer on the media thread.
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential UAF in cast_streaming via DemuxerConnector dangling pointers

Flapjack, 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: The cast_streaming component contains two potential Use-After-Free (UAF) vulnerabilities involving DemuxerConnector. These occur because FrameInjectingDemuxer does not reliably clean up its raw pointer from the connector or uses base::Unretained, leading to dangling pointers that can be dereferenced during Mojo disconnect events or delayed tasks.

Affected files:

  • components/cast_streaming/renderer/frame/demuxer_connector.cc
  • components/cast_streaming/renderer/frame/frame_injecting_demuxer.cc

Estimated timestamp from git blame: 2020-06-09

Two potential Use-After-Free (UAF) vulnerabilities exist in the cast_streaming component of Cast Receiver builds. They stem from improper lifecycle management of the FrameInjectingDemuxer object and its interaction with the DemuxerConnector.

Issue 1: Dangling Pointer via Bypassed Cleanup

DemuxerConnector (living on the renderer’s main thread) stores a raw pointer to a FrameInjectingDemuxer in its demuxer_ member. The demuxer attempts to clean up this pointer in its destructor:

// components/cast_streaming/renderer/frame/frame_injecting_demuxer.cc
FrameInjectingDemuxer::~FrameInjectingDemuxer() {
  if (was_initialization_successful_) {
    original_task_runner_->PostTask(
        FROM_HERE, base::BindOnce(&DemuxerConnector::OnDemuxerDestroyed,
                                  base::Unretained(demuxer_connector_)));
  }
}

If the initialization of the demuxer is aborted before completion (e.g., the media element is destroyed), was_initialization_successful_ remains false. The cleanup task is silently skipped, leaving DemuxerConnector::demuxer_ as a dangling pointer.

Subsequently, if the Cast Streaming Mojo connection is dropped, DemuxerConnector::OnReceiverDisconnected runs:

// components/cast_streaming/renderer/frame/demuxer_connector.cc
void DemuxerConnector::OnReceiverDisconnected() {
  // ...
  if (demuxer_ && !is_demuxer_initialized_) {
    OnStreamsInitialized(mojom::AudioStreamInitializationInfoPtr(),
                         mojom::VideoStreamInitializationInfoPtr());
  }
}

Because the dangling pointer is non-null and initialization never completed, this calls OnStreamsInitialized, which executes demuxer_->OnStreamsInitialized(...), dereferencing the freed memory.

Potential Trigger Steps (Theoretical):

  1. A malicious Cast Receiver app sets a <video> source to "data:cast_streaming_receiver".
  2. Initialization begins, setting the demuxer_ pointer in the DemuxerConnector.
  3. The attacker’s JS abruptly removes the <video> element, destroying the demuxer before initialization finishes.
  4. The attacker disconnects the underlying Cast session (Mojo disconnect).
  5. The disconnection handler dereferences the dangling pointer.

Issue 2: UAF via Unretained Task

During initialization, FrameInjectingDemuxer::Initialize (on the media thread) posts a task back to the main thread to register itself:

  original_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&DemuxerConnector::SetDemuxer,
                                base::Unretained(demuxer_connector_),
                                base::Unretained(this)));

If the demuxer is destroyed after this task is posted but before it executes on the main thread, DemuxerConnector::SetDemuxer will operate on a freed pointer. If a previous demuxer was already set, SetDemuxer immediately invokes a method on the freed pointer (demuxer->OnStreamsInitialized(...)).

Impact

These UAFs can potentially lead to Remote Code Execution (RCE) within the renderer process. By grooming the heap after the demuxer is freed but before the dangling pointer is dereferenced, an attacker could hijack virtual method calls or object members. While MiraclePtr (raw_ptr) mitigates these to crashes on supported platforms, it remains fully exploitable on platforms without it (such as older 32-bit Cast devices).

Suggested Fix

  1. Unconditional Cleanup: Remove the if (was_initialization_successful_) check in ~FrameInjectingDemuxer(). The DemuxerConnector should always be notified when the demuxer is destroyed, regardless of initialization state.
  2. Use Weak Pointers: Instead of base::Unretained(this), the demuxer should use a base::WeakPtr when posting the SetDemuxer task to ensure the task is safely canceled if the object is destroyed.

Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff


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