Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Media Stream
DescriptionUse after free in Media Stream
ComponentMedia Stream
Bug ClassUAF
Tracker448046109
Fix commit9696b698f7dc (chromium/src) +18/-0
CISA KEVNot listed
Creditedsherkito
Disclosed2025-12-02

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/modules/mediastream/web_media_player_ms.cc
modified

Files Changed

  • third_party/blink/renderer/modules/mediastream/web_media_player_ms.cc
From 9696b698f7dc30fbef6a93790aafbd289538a803 Mon Sep 17 00:00:00 2001
From: Frank Liberato <[email protected]>
Date: Mon, 27 Oct 2025 12:49:33 -0700
Subject: [PATCH] Prevent media element GC in callbacks in WebMediaPlayerMS

Callbacks into WebMediaPlayerMS can happen, in general, when
the element is eligible for GC.  This CL adds a stack reference
to the client (element) to prevent conservative GC from
reclaiming the element during the callback.

Bug: 448046109
Change-Id: I67e37bbc2b25ccd54d0d94e1857f07dd3e673418
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7000843
Reviewed-by: Dale Curtis <[email protected]>
Commit-Queue: Frank Liberato <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1536174}
---

diff --git a/third_party/blink/renderer/modules/mediastream/web_media_player_ms.cc b/third_party/blink/renderer/modules/mediastream/web_media_player_ms.cc
index f0a7fc93..1b2ff56f 100644
--- a/third_party/blink/renderer/modules/mediastream/web_media_player_ms.cc
+++ b/third_party/blink/renderer/modules/mediastream/web_media_player_ms.cc
@@ -12,6 +12,7 @@
 #include <string>
 #include <utility>
 
+#include "base/debug/alias.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
@@ -63,6 +64,19 @@
 #include "third_party/blink/renderer/platform/wtf/cross_thread_copier_media.h"
 #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
 
+// Put this macro in a scope to prevent `client_` from being GC'd.
+// This is important for any method that might be called from anywhere
+// where GC of the element is not prevented.  GC is prevented if the
+// call into `this` came from the element itself (directly or indirectly,
+// as long as the element's `this` is on the stack), or HasPendingActivation()
+// returns true.  In other cases, especially callbacks from the "outside
+// world", one should PREVENT_CLIENT_GC to keep the element from being
+// garbage collected.  Failure to do this can cause `this` to be destroyed
+// when the player is finalized.
+#define PREVENT_CLIENT_GC      \
+  auto client_copy_ = client_; \
+  base::debug::Alias(&client_copy_)
+
 namespace blink {
 
 template <>
@@ -456,6 +470,7 @@
 
 void WebMediaPlayerMS::OnAudioRenderErrorCallback() {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  PREVENT_CLIENT_GC;
 
   if (watch_time_reporter_)
     watch_time_reporter_->OnError(media::AUDIO_RENDERER_ERROR);
@@ -1311,6 +1326,7 @@
     bool is_opaque) {
   DVLOG(1) << __func__;
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  PREVENT_CLIENT_GC;
 
   has_first_frame_ = true;
   OnTransformChanged(video_transform);
@@ -1329,6 +1345,7 @@
 void WebMediaPlayerMS::OnOpacityChanged(bool is_opaque) {
   DVLOG(1) << __func__;
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  PREVENT_CLIENT_GC;
 
   opaque_ = is_opaque;
   if (!bridge_) {
@@ -1345,6 +1362,7 @@
     media::VideoTransformation video_transform) {
   DVLOG(1) << __func__;
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  PREVENT_CLIENT_GC;
 
   if (!bridge_) {
     // Keep the old |video_layer_| alive until SetCcLayer() is called with a new
Loading diff…

Original Bug Report

reported by [email protected]

Use-After-Free in WebMediaPlayerMS::OnFirstFrameReceived

Steps to reproduce the problem

  1. chrome.exe –js-flags="–stress-incremental-marking –expose-gc" –no-sandbox

Problem Description

In the MaybeCreateWatchTimeReporter function below, a call to GarbageCollector is possible, which causes Use-After-Free.

void WebMediaPlayerMS::OnFirstFrameReceived(
    media::VideoTransformation video_transform,
    bool is_opaque) {
  DVLOG(1) << __func__;
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  has_first_frame_ = true;
  OnTransformChanged(video_transform);
  OnOpacityChanged(is_opaque);

  if (use_surface_layer_)
    ActivateSurfaceLayerForVideo(video_transform);

  SetReadyState(WebMediaPlayer::kReadyStateHaveMetadata);
  SetReadyState(WebMediaPlayer::kReadyStateHaveEnoughData);
  TriggerResize();
  ResetCanvasCache();
  MaybeCreateWatchTimeReporter();

The most annoying code in the code is the function below called from the WebMediaPlayerMS::SetReadyState function, and the ScheduleNamedEvent function can be avoided by satisfying the tracks_are_ready condition in the function below.

void HTMLMediaElement::SetReadyState(ReadyState state) {
  '''
  bool was_potentially_playing = PotentiallyPlaying();

  ReadyState old_state = ready_state_;
  ReadyState new_state = state;

  bool tracks_are_ready = TextTracksAreReady();

  '''
  if (ready_state_ == kHaveEnoughData && old_state < kHaveEnoughData &&
      tracks_are_ready) {
    if (old_state <= kHaveCurrentData) {
      ScheduleNamedEvent(event_type_names::kCanplay);
      if (is_potentially_playing)
        ScheduleNotifyPlaying();
    }

    if (autoplay_policy_->RequestAutoplayByAttribute()) {
      paused_ = false;
      SetShowPosterFlag(false);
      GetCueTimeline().InvokeTimeMarchesOn();
      ScheduleNamedEvent(event_type_names::kPlay);
      ScheduleNotifyPlaying();
      can_autoplay_ = false;
    }

    ScheduleNamedEvent(event_type_names::kCanplaythrough);
  }

  UpdatePlayState();
}

The exact location of the function that calls the GarbageCollector can be found through the AudioComponents or VideoComponents function call in the WebMediaPlayerMS::GetMediaStreamType function called by WebMediaPlayerMS::MaybeCreateWatchTimeReporter.

WebMediaPlayerMS::GetMediaStreamType() {
  if (web_stream_.IsNull())
    return std::nullopt;

  // If either the first video or audio source is remote, the media stream is
  // of remote source.
  MediaStreamDescriptor& descriptor = *web_stream_;
  MediaStreamSource* media_source = nullptr;
  if (HasVideo()) {
    auto video_components = descriptor.VideoComponents();
    DCHECK_GT(video_components.size(), 0U);
    media_source = video_components[0]->Source();
  } else if (HasAudio()) {
    auto audio_components = descriptor.AudioComponents();
    DCHECK_GT(audio_components.size(), 0U);
    media_source = audio_components[0]->Source();
  }

If the trigger doesn’t proceed smoothly, try using native Chromium instead of ASAN Chromium. Depending on your computer’s specifications, there may still be areas where the task order and GC condition processing probability are low. We will improve this in the future and re-upload the proof-of-concept (PoC).

The Chromium commit that triggered this vulnerability is 06773e4fd40f4b8ad8a3c94a86ff14613b626c8e

Summary

Use-After-Free in WebMediaPlayerMS::OnFirstFrameReceived

Custom Questions

Type of crash:

Renderer

Crash state:

An action occurs that references a freed object member object.

Reporter credit:

sherkito

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A \

View on issue tracker