Low watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Media Stream
DescriptionUse after free in Media Stream
ComponentMedia Stream
Sub-componentBlink>MediaStream
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker448046109
Fix commit9696b698f7dc (chromium/src) +18/-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
Creditedsherkito
Reported2025-09-29 (per vendor advisory)
Fix landed2025-10-27 (public git)
Disclosed2025-12-02
Patch gap36 days — fix public in source before the advisory

Background

`WebMediaPlayerMS`
the Blink media player that plays a MediaStream (for example from getUserMedia, WebRTC or capture) inside an HTMLMediaElement. It holds a client_ pointer back to that element.
Oilpan conservative stack scanning
Blink’s garbage collector treats pointers it finds on the native stack as roots. A garbage-collected object referenced only from a live stack slot is not reclaimed.
Player lifetime coupling
the player is owned by its media element. If the element is collected and finalized, the player (this) is destroyed too.
`base::debug::Alias`
a helper that stops the compiler from optimizing away a local variable, so a stack copy of a pointer stays in place for the stack scan.

Root Cause Analysis

Several WebMediaPlayerMS methods are invoked as callbacks from outside the element, from the audio renderer and compositor/frame-delivery paths. When those callbacks run, no stack frame necessarily holds a reference to the element, and the element may already be unreachable from script and eligible for garbage collection. The new comment says GC is prevented only when the call came from the element itself or HasPendingActivation() is true. Otherwise, a GC that runs during the callback can finalize the element, and that destroys this while the method is still running. Any later access to member state in the callback is then a use-after-free.

The patch adds a PREVENT_CLIENT_GC macro that copies client_ into a local client_copy_ and passes it to base::debug::Alias.

The fix places this macro at the top of four callback entry points, so conservative stack scanning keeps the element, and therefore the player, alive until the callback returns.

Key insight
Externally triggered callbacks into WebMediaPlayerMS assumed the owning media element would outlive the call, but nothing on the stack rooted it. The fix pins client_ on the stack for the duration of each such callback.

Attack Path

  1. Create a MediaStream player A page attaches a MediaStream to a media element, which creates a WebMediaPlayerMS.
  2. Drop all references Script drops every reference to the element, so it is eligible for GC while the stream keeps delivering frames or audio.
  3. External callback fires A frame or audio event invokes one of the affected callbacks: OnAudioRenderErrorCallback, OnOpacityChanged, or the first-frame and transform-change handlers.
  4. GC during the callback If garbage collection runs while the callback is executing, the element is finalized and the player is freed, so later member accesses in the callback use freed memory. The exact timing and any resulting memory-corruption primitive are (inferred).

Impact Assessment

The result is a use-after-free of the WebMediaPlayerMS object in the sandboxed renderer process. Ordinary web content can reach it by creating MediaStream playback, but it depends on winning a race between a callback and a GC cycle. Some stream sources need a capture permission, though others such as canvas or WebRTC streams may not (inferred). The vendor rates it low severity, and the advisory does not report exploitation in the wild. Code execution in the renderer, or anything beyond it, is (inferred) and not shown by the patch.

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

Audit Directions

  • Other external entry points
    Check the remaining WebMediaPlayerMS methods, and other WebMediaPlayer implementations such as WebMediaPlayerImpl, that are reached from compositor, audio or IPC callbacks without a stack reference to the element.
  • Non-GC objects owned by GC objects
    Look for objects destroyed by an Oilpan finalizer that run callbacks which may allocate or trigger GC. Each such entry point needs the owner rooted, through a Persistent, a stack reference, or pending-activity tracking.
  • `HasPendingActivity` coverage
    Check that media elements report pending activity for every state in which external callbacks can still arrive, rather than relying on per-callback stack pins.
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