Critical watch ✔︎ verified CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chromecast
DescriptionUse after free in Chromecast
ComponentChromecast
Sub-componentMedia
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — 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: Mechanism matches commit message and tests. Analysis omits that the UAF sink is in chromecast/starboard code, so it applies only to Starboard-based Cast builds; the unit test does show the bad-message string the analysis calls cut off.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker503617302
Fix commit2dfbd25331c4 (chromium/src) +90/-2
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-17 (per vendor advisory)
Fix landed2026-04-28 (public git)
Disclosed2026-06-02
Patch gap35 days — fix public in source before the advisory

Background

`MojoRendererService`
A browser/GPU-side Mojo endpoint that wraps a concrete media::Renderer and exposes its Initialize/playback methods to a (potentially untrusted) renderer over IPC.
`StarboardRenderer`
The Cast/Chromecast media::Renderer implementation that allocates and drives an SbPlayer through its player_manager_.
`GeometryChangeHandler`
The Starboard component that applies video-geometry updates (e.g. SetPlayerBounds) to the live player and therefore holds a pointer that must track the player’s lifetime.
`DCHECK` vs `CHECK`
DCHECK_EQ compiles to a no-op in release builds, so a precondition enforced only by DCHECK is effectively unenforced in shipped Chrome.

Root Cause Analysis

MojoRendererService::Initialize guarded against re-initialization only with DCHECK_EQ(state_, STATE_UNINITIALIZED), which is stripped from release builds, so a compromised renderer could invoke Initialize a second time over Mojo. The first call successfully stands up a StarboardRenderer/SbPlayer, but a second call could fail during player creation and tear the underlying player down, while objects such as GeometryChangeHandler continued to hold a pointer to the now-destroyed player — a use-after-free.

The fix converts the soft precondition into hard, always-on enforcement: MojoRendererService::Initialize now treats a repeat call as a protocol violation and reports a bad message (the added include of mojo/public/cpp/bindings/message.h and the removed DCHECK_EQ line confirm the direction, though the exact replacement statement is cut off in the diff), and StarboardRenderer::Initialize gains CHECK(!player_manager_) so a second initialization aborts before it can disturb an already-allocated player. This makes double-initialization impossible in release builds rather than relying on a debug-only assertion, closing the window in which a dangling player pointer could be used.

Key insight
The single mistake was enforcing a once-only initialization invariant with a debug-only DCHECK, leaving release builds willing to re-enter Initialize and destroy a live player out from under its observers; the fix re-establishes the invariant with release-build enforcement (a bad-message report in MojoRendererService and a CHECK in StarboardRenderer).

Attack Path

  1. Compromised renderer An attacker who already controls a renderer process obtains the mojom::Renderer interface backed by MojoRendererService in the Cast browser process.
  2. First Initialize The renderer calls Initialize once; StarboardRenderer allocates an SbPlayer via player_manager_ and wires up GeometryChangeHandler.
  3. Second Initialize Because the only guard was a release-stripped DCHECK_EQ, the renderer calls Initialize again; the re-initialization path fails player creation and destroys the underlying player.
  4. Dangling pointer use Subsequent geometry activity reaches GeometryChangeHandler, which still references the freed player, producing a use-after-free (the regression test DoesNotSetSbPlayerOnInitializationFailure encodes that no bounds must be set after a failed init).
  5. Downstream control Corruption of the freed player object could be leveraged toward further memory corruption or code execution in the browser process (inferred).

Impact Assessment

An attacker with a compromised renderer can trigger a use-after-free in the privileged Cast browser process by double-initializing the Mojo renderer service, with no user gesture or special flag beyond an existing renderer compromise. The vendor advisory rates this critical; it does not state in-the-wild exploitation. Escalation from the UAF to a read/write primitive or code execution is not shown by the diff and is (inferred).

Changed Functions

FunctionChangeNotes
TEST_F
chromecast/starboard/media/renderer/starboard_renderer_test.cc
modified
TEST_F
media/mojo/clients/mojo_renderer_unittest.cc
modified

Files Changed

  • chromecast/starboard/media/renderer/starboard_renderer.cc
  • chromecast/starboard/media/renderer/starboard_renderer_test.cc
  • media/mojo/clients/BUILD.gn
  • media/mojo/clients/mojo_renderer_unittest.cc
  • media/mojo/services/mojo_renderer_service.cc

Audit Directions

  • Debug-only invariants on IPC entry points
    Audit Mojo service methods reachable from the renderer that enforce call-ordering or once-only semantics with DCHECK/DCHECK_EQ; these are unenforced in release and should use CHECK, mojo::ReportBadMessage, or explicit state handling.
  • Failed-initialization cleanup
    Look for media::Renderer/player wrappers where a failed Initialize tears down a backing object while leaving observers or handlers (e.g. geometry, overlay, metrics) holding pointers to it.
  • Re-initialization of stateful media objects
    Review components that assume Initialize runs exactly once (player_manager_, decoder, or session objects) for behavior when called twice from an untrusted process.
From 2dfbd25331c40ab4bd4454ea20c8d03d3898e334 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
Date: Mon, 27 Apr 2026 18:06:39 -0700
Subject: [PATCH] media: Fix Use-After-Free in StarboardRenderer and MojoRendererService

A compromised renderer could trigger a Use-After-Free in the Cast
browser process by initializing a MojoRendererService twice. The first
initialization successfully allocates an SbPlayer, but a second call
(allowed in release builds due to a debug-only DCHECK) could fail,
destroying the underlying player while leaving a dangling pointer in
GeometryChangeHandler.

This CL fixes the issue by:

1. Adding a hard check in MojoRendererService::Initialize to prevent
   re-initialization and reporting a bad message if attempted.
2. Adding a CHECK in StarboardRenderer::Initialize to ensure it is only
   called once, providing defense-in-depth.

Fixed: 503617302
Change-Id: Iffd137bfe533ae35848916667faaf5f22372b002
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7772958
Reviewed-by: Simeon Anfinrud <[email protected]>
Reviewed-by: Colin Blundell <[email protected]>
Commit-Queue: Andrew Paseltiner <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1621456}
---

diff --git a/chromecast/starboard/media/renderer/starboard_renderer.cc b/chromecast/starboard/media/renderer/starboard_renderer.cc
index f456c8b6..7fb4693 100644
--- a/chromecast/starboard/media/renderer/starboard_renderer.cc
+++ b/chromecast/starboard/media/renderer/starboard_renderer.cc
@@ -55,6 +55,7 @@
                                    ::media::PipelineStatusCallback init_cb) {
   CHECK(media_task_runner_->RunsTasksInCurrentSequence());
   CHECK(client);
+  CHECK(!player_manager_);
 
   ::media::DemuxerStream* audio_stream =
       media_resource->GetFirstStream(::media::DemuxerStream::Type::AUDIO);
diff --git a/chromecast/starboard/media/renderer/starboard_renderer_test.cc b/chromecast/starboard/media/renderer/starboard_renderer_test.cc
index 36cfdd1e..cca300bc 100644
--- a/chromecast/starboard/media/renderer/starboard_renderer_test.cc
+++ b/chromecast/starboard/media/renderer/starboard_renderer_test.cc
@@ -649,6 +649,56 @@
   EXPECT_EQ(renderer.GetMediaTime(), kMediaTime);
 }
 
+// Regression test for crbug.com/503617302.
+TEST_F(StarboardRendererTest, DoesNotSetSbPlayerOnInitializationFailure) {
+  const gfx::RectF geometry(0, 0, 1920, 1080);
+  const gfx::OverlayTransform transform =
+      gfx::OverlayTransform::OVERLAY_TRANSFORM_NONE;
+
+  // Set up the mock to fail player creation.
+  EXPECT_CALL(*starboard_, CreatePlayer(_, _)).WillOnce(Return(nullptr));
+
+  // The player's bounds should NOT be set after a failed initialization, even
+  // if geometry changes occur.
+  EXPECT_CALL(*starboard_, SetPlayerBounds(_, _, _, _, _, _)).Times(0);
+
+  StarboardRenderer renderer(std::move(starboard_), task_runner_, plane_id_,
+                             /*enable_buffering=*/true,
+                             &geometry_setter_service_, &cast_metrics_helper_);
+  RunPendingTasks();
+
+  EXPECT_CALL(
+      pipeline_status_fn_,
+      Call(HasStatusCode(
+          ::media::PipelineStatusCodes::PIPELINE_ERROR_INITIALIZATION_FAILED)))
+      .Times(1);
+  renderer.Initialize(
+      &media_resource_, &client_,
+      base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction()));
+  RunPendingTasks();
+
+  // This geometry change should be ignored by the geometry_change_handler_,
+  // which was never initialized with a player.
+  static_cast<mojom::VideoGeometrySetter*>(&geometry_setter_service_)
+      ->SetVideoGeometry(geometry, transform, plane_id_);
+  RunPendingTasks();
+}
+
+TEST_F(StarboardRendererTest, Initialize_Twice_Crashes) {
+  StarboardRenderer renderer(std::move(starboard_), task_runner_, plane_id_,
+                             /*enable_buffering=*/true,
+                             &geometry_setter_service_, &cast_metrics_helper_);
+
+  renderer.Initialize(
+      &media_resource_, &client_,
+      base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction()));
+  RunPendingTasks();
+
+  EXPECT_CHECK_DEATH(renderer.Initialize(
+      &media_resource_, &client_,
+      base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction())));
+}
+
 TEST_F(StarboardRendererTest, SetPlaybackRateReportsMetric) {
   // Ignore unrelated metrics calls.
   EXPECT_CALL(cast_metrics_helper_, RecordApplicationEvent(_))
diff --git a/media/mojo/clients/BUILD.gn b/media/mojo/clients/BUILD.gn
index 66e1242..9fa9618 100644
--- a/media/mojo/clients/BUILD.gn
+++ b/media/mojo/clients/BUILD.gn
@@ -157,6 +157,7 @@
     "//gpu/ipc/common:test_support",
     "//media:test_support",
     "//media/mojo:test_support",
+    "//mojo/public/cpp/test_support:test_utils",
     "//testing/gmock",
     "//testing/gtest",
   ]
diff --git a/media/mojo/clients/mojo_renderer_unittest.cc b/media/mojo/clients/mojo_renderer_unittest.cc
index 1814cf0..3723b2f 100644
--- a/media/mojo/clients/mojo_renderer_unittest.cc
+++ b/media/mojo/clients/mojo_renderer_unittest.cc
@@ -2,11 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "media/mojo/clients/mojo_renderer.h"
+
 #include <stdint.h>
 
 #include <memory>
 
 #include "base/functional/bind.h"
+#include "base/functional/callback_helpers.h"
 #include "base/memory/ptr_util.h"
 #include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
@@ -21,7 +24,6 @@
 #include "media/base/test_helpers.h"
 #include "media/cdm/clear_key_cdm_common.h"
 #include "media/cdm/default_cdm_factory.h"
-#include "media/mojo/clients/mojo_renderer.h"
 #include "media/mojo/common/media_type_converters.h"
 #include "media/mojo/mojom/content_decryption_module.mojom.h"
 #include "media/mojo/mojom/renderer.mojom.h"
@@ -33,6 +35,7 @@
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "mojo/public/cpp/bindings/self_owned_receiver.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
 
@@ -232,6 +235,34 @@
   Initialize();
 }
 
+// Regression test for crbug.com/503617302.
+TEST_F(MojoRendererTest, Initialize_Twice) {
+  // Create a service directly to bypass client-side checks in MojoRenderer.
+  auto mock_renderer = std::make_unique<StrictMock<MockRenderer>>();
+  mojo::Remote<mojom::Renderer> remote;
+  auto receiver_ref = MojoRendererService::Create(
+      &mojo_cdm_service_context_, std::move(mock_renderer),
+      remote.BindNewPipeAndPassReceiver());
+
+  mojo::PendingAssociatedRemote<mojom::RendererClient> client_remote_1;
+  auto client_receiver_1 = client_remote_1.InitWithNewEndpointAndPassReceiver();
+  std::vector<mojo::PendingRemote<mojom::DemuxerStream>> streams;
+  mojo::PendingRemote<mojom::DemuxerStream> stream_remote;
+  auto stream_receiver = stream_remote.InitWithNewPipeAndPassReceiver();
+  streams.push_back(std::move(stream_remote));
+  remote->Initialize(std::move(client_remote_1), std::move(streams),
+                     base::DoNothing());
+
+  mojo::PendingAssociatedRemote<mojom::RendererClient> client_remote_2;
+  auto client_receiver_2 = client_remote_2.InitWithNewEndpointAndPassReceiver();
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  remote->Initialize(std::move(client_remote_2), std::nullopt,
+                     base::DoNothing());
+
+  EXPECT_EQ("MojoRendererService is already initialized",
+            bad_message_observer.WaitForBadMessage());
+}
 TEST_F(MojoRendererTest, Initialize_Failure) {
   CreateAudioStream();
   // Mojo Renderer only expects a boolean result, which will be translated
diff --git a/media/mojo/services/mojo_renderer_service.cc b/media/mojo/services/mojo_renderer_service.cc
index 90d4ae6..c0697a0 100644
--- a/media/mojo/services/mojo_renderer_service.cc
+++ b/media/mojo/services/mojo_renderer_service.cc
@@ -16,6 +16,7 @@
 #include "media/mojo/common/media_type_converters.h"
 #include "media/mojo/services/media_resource_shim.h"
 #include "media/mojo/services/mojo_cdm_service_context.h"
+#include "mojo/public/cpp/bindings/message.h"
 
 namespace media {
 
@@ -58,7 +59,11 @@
         streams,
     InitializeCallback callback) {
   DVLOG(1) << __func__;
-  DCHECK_EQ(state_, STATE_UNINITIALIZED);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chromecast/starboard/media/renderer/starboard_renderer_test.cc b/chromecast/starboard/media/renderer/starboard_renderer_test.cc
index 36cfdd1e..cca300bc 100644
--- a/chromecast/starboard/media/renderer/starboard_renderer_test.cc
+++ b/chromecast/starboard/media/renderer/starboard_renderer_test.cc
@@ -649,6 +649,56 @@
   EXPECT_EQ(renderer.GetMediaTime(), kMediaTime);
 }
 
+// Regression test for crbug.com/503617302.
+TEST_F(StarboardRendererTest, DoesNotSetSbPlayerOnInitializationFailure) {
+  const gfx::RectF geometry(0, 0, 1920, 1080);
+  const gfx::OverlayTransform transform =
+      gfx::OverlayTransform::OVERLAY_TRANSFORM_NONE;
+
+  // Set up the mock to fail player creation.
+  EXPECT_CALL(*starboard_, CreatePlayer(_, _)).WillOnce(Return(nullptr));
+
+  // The player's bounds should NOT be set after a failed initialization, even
+  // if geometry changes occur.
+  EXPECT_CALL(*starboard_, SetPlayerBounds(_, _, _, _, _, _)).Times(0);
+
+  StarboardRenderer renderer(std::move(starboard_), task_runner_, plane_id_,
+                             /*enable_buffering=*/true,
+                             &geometry_setter_service_, &cast_metrics_helper_);
+  RunPendingTasks();
+
+  EXPECT_CALL(
+      pipeline_status_fn_,
+      Call(HasStatusCode(
+          ::media::PipelineStatusCodes::PIPELINE_ERROR_INITIALIZATION_FAILED)))
+      .Times(1);
+  renderer.Initialize(
+      &media_resource_, &client_,
+      base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction()));
+  RunPendingTasks();
+
+  // This geometry change should be ignored by the geometry_change_handler_,
+  // which was never initialized with a player.
+  static_cast<mojom::VideoGeometrySetter*>(&geometry_setter_service_)
+      ->SetVideoGeometry(geometry, transform, plane_id_);
+  RunPendingTasks();
+}
+
+TEST_F(StarboardRendererTest, Initialize_Twice_Crashes) {
+  StarboardRenderer renderer(std::move(starboard_), task_runner_, plane_id_,
+                             /*enable_buffering=*/true,
+                             &geometry_setter_service_, &cast_metrics_helper_);
+
+  renderer.Initialize(
+      &media_resource_, &client_,
+      base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction()));
+  RunPendingTasks();
+
+  EXPECT_CHECK_DEATH(renderer.Initialize(
+      &media_resource_, &client_,
+      base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction())));
+}
+
 TEST_F(StarboardRendererTest, SetPlaybackRateReportsMetric) {
   // Ignore unrelated metrics calls.
   EXPECT_CALL(cast_metrics_helper_, RecordApplicationEvent(_))
diff --git a/media/mojo/clients/mojo_renderer_unittest.cc b/media/mojo/clients/mojo_renderer_unittest.cc
index 1814cf0..3723b2f 100644
--- a/media/mojo/clients/mojo_renderer_unittest.cc
+++ b/media/mojo/clients/mojo_renderer_unittest.cc
@@ -2,11 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "media/mojo/clients/mojo_renderer.h"
+
 #include <stdint.h>
 
 #include <memory>
 
 #include "base/functional/bind.h"
+#include "base/functional/callback_helpers.h"
 #include "base/memory/ptr_util.h"
 #include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
@@ -21,7 +24,6 @@
 #include "media/base/test_helpers.h"
 #include "media/cdm/clear_key_cdm_common.h"
 #include "media/cdm/default_cdm_factory.h"
-#include "media/mojo/clients/mojo_renderer.h"
 #include "media/mojo/common/media_type_converters.h"
 #include "media/mojo/mojom/content_decryption_module.mojom.h"
 #include "media/mojo/mojom/renderer.mojom.h"
@@ -33,6 +35,7 @@
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "mojo/public/cpp/bindings/self_owned_receiver.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
 
@@ -232,6 +235,34 @@
   Initialize();
 }
 
+// Regression test for crbug.com/503617302.
+TEST_F(MojoRendererTest, Initialize_Twice) {
+  // Create a service directly to bypass client-side checks in MojoRenderer.
+  auto mock_renderer = std::make_unique<StrictMock<MockRenderer>>();
+  mojo::Remote<mojom::Renderer> remote;
+  auto receiver_ref = MojoRendererService::Create(
+      &mojo_cdm_service_context_, std::move(mock_renderer),
+      remote.BindNewPipeAndPassReceiver());
+
+  mojo::PendingAssociatedRemote<mojom::RendererClient> client_remote_1;
+  auto client_receiver_1 = client_remote_1.InitWithNewEndpointAndPassReceiver();
+  std::vector<mojo::PendingRemote<mojom::DemuxerStream>> streams;
+  mojo::PendingRemote<mojom::DemuxerStream> stream_remote;
+  auto stream_receiver = stream_remote.InitWithNewPipeAndPassReceiver();
+  streams.push_back(std::move(stream_remote));
+  remote->Initialize(std::move(client_remote_1), std::move(streams),
+                     base::DoNothing());
+
+  mojo::PendingAssociatedRemote<mojom::RendererClient> client_remote_2;
+  auto client_receiver_2 = client_remote_2.InitWithNewEndpointAndPassReceiver();
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  remote->Initialize(std::move(client_remote_2), std::nullopt,
+                     base::DoNothing());
+
+  EXPECT_EQ("MojoRendererService is already initialized",
+            bad_message_observer.WaitForBadMessage());
+}
 TEST_F(MojoRendererTest, Initialize_Failure) {
   CreateAudioStream();
   // Mojo Renderer only expects a boolean result, which will be translated
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in Cast StarboardRenderer via double initialization

Project Fortify, 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 without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: A compromised renderer can trigger a Use-After-Free in the Cast browser process by initializing a StarboardRenderer twice via its Mojo interface. If the second initialization fails, the underlying player is freed but a dangling pointer remains in GeometryChangeHandler, which can later be dereferenced by submitting a specific compositor frame.

Affected files:

  • chromecast/starboard/media/renderer/starboard_renderer.cc
  • chromecast/starboard/media/renderer/geometry_change_handler.cc
  • chromecast/starboard/media/renderer/geometry_change_handler.h
  • chromecast/starboard/media/renderer/starboard_player_manager.cc
  • media/mojo/services/mojo_renderer_service.cc
  • chromecast/starboard/media/media/starboard_api_wrapper_base.cc

Estimated timestamp from git blame: 2025-10-06

Summary

A potential Use-After-Free (UAF) vulnerability exists in the Cast browser process on Starboard builds. The MojoRendererService uses a DCHECK to prevent re-initialization, which is compiled out in release builds. This allows a compromised renderer to call Initialize multiple times. If a subsequent initialization fails, StarboardRenderer destroys the existing StarboardPlayerManager (freeing the SbPlayer) but fails to clear the sb_player_ pointer in its GeometryChangeHandler. A subsequent geometry update via a VideoHoleDrawQuad will then write attacker-controlled bounds to the freed SbPlayer memory.

Vulnerability Details

  1. MojoRendererService::Initialize (in media/mojo/services/mojo_renderer_service.cc) uses a debug-only DCHECK_EQ(state_, STATE_UNINITIALIZED) to ensure it is only called once. In release builds, a compromised renderer can call this method multiple times on the same remote.
  2. When Initialize is called a second time, it eventually invokes StarboardRenderer::Initialize (in chromecast/starboard/media/renderer/starboard_renderer.cc).
  3. StarboardRenderer::Initialize attempts to create a new StarboardPlayerManager by calling StarboardPlayerManager::Create(...).
  4. If the attacker provides an unsupported media configuration (e.g., using AudioCodec::kAMR_NB), Create fails and returns nullptr.
  5. The assignment player_manager_ = StarboardPlayerManager::Create(...) evaluates to nullptr. Because player_manager_ is a std::unique_ptr, this destroys the old StarboardPlayerManager from the first initialization, which in turn calls SbPlayerDestroy to free the underlying vendor-allocated SbPlayer.
  6. Because player_manager_ is now null, StarboardRenderer::Initialize returns early with an error:
    if (!player_manager_) {
      // Early return on failure
      media_task_runner_->PostTask(..., PIPELINE_ERROR_INITIALIZATION_FAILED);
      return;
    }
    
  7. Crucially, the early return skips the subsequent call to geometry_change_handler_.SetSbPlayer(...). As a result, the GeometryChangeHandler retains a dangling raw_ptr<void> sb_player_ pointing to the freed memory.
  8. The attacker can then trigger GeometryChangeHandler::OnVideoGeometryChange by submitting a compositor frame from the renderer containing a VideoHoleDrawQuad with the original overlay_plane_id and attacker-controlled bounds.
  9. OnVideoGeometryChange calls SetPlayerBounds, which invokes the vendor Starboard API SbPlayerSetBounds with the dangling sb_player_ pointer and the attacker-controlled coordinates, resulting in a highly controllable Write-After-Free in the browser process.

Note: MiraclePtr (BackupRefPtr) does not mitigate this vulnerability because SbPlayer is allocated by an external vendor library, not PartitionAlloc.

Potential Reproduction Steps

Note: These are suggested steps for an attacker; we do not have a working proof of concept.

  1. From a compromised renderer, call media.mojom.InterfaceFactory::CreateCastRenderer with a generated overlay_plane_id to obtain a browser-process media.mojom.Renderer remote.
  2. Call Renderer::Initialize with a valid media configuration (e.g., AAC/H264) and wait for the success callback. This successfully allocates the SbPlayer.
  3. Call Renderer::Initialize again on the same remote, this time providing an invalid configuration (e.g., advertising AudioCodec::kAMR_NB). This frees the SbPlayer but leaves a dangling pointer in GeometryChangeHandler.
  4. Submit a compositor frame from the renderer containing a VideoHoleDrawQuad. Set its overlay_plane_id to match the one from Step 1, and set its bounds to arbitrary values payload.
  5. The Viz process will forward the bounds to the browser process, triggering a write to the freed SbPlayer memory.

Suggested Fix

There are two places where this should be addressed for defense-in-depth:

  1. Prevent re-initialization at the Mojo boundary: In MojoRendererService::Initialize, replace or supplement the DCHECK with a hard check that terminates the renderer if it attempts to initialize twice:
    if (state_ != STATE_UNINITIALIZED) {
      mojo::ReportBadMessage("MojoRendererService cannot be initialized multiple times");
      return;
    }
    
  2. Clear the dangling pointer on failure: In StarboardRenderer::Initialize, ensure the GeometryChangeHandler’s pointer is cleared if player creation fails:
    if (!player_manager_) {
      geometry_change_handler_.SetSbPlayer(nullptr); // ADD THIS LINE
      LOG(ERROR) << "Unable to create StarboardPlayerManager";
      // ...
      return;
    }
    

Evaluated with Chrome root at commit: 661452647ddb2827305122ff3273bd5dea403f09


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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