CVE-2026-10884
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fchromecast/starboard/media/renderer/starboard_renderer_test.cc |
modified | |
TEST_Fmedia/mojo/clients/mojo_renderer_unittest.cc |
modified |
Files Changed
chromecast/starboard/media/renderer/starboard_renderer.ccchromecast/starboard/media/renderer/starboard_renderer_test.ccmedia/mojo/clients/BUILD.gnmedia/mojo/clients/mojo_renderer_unittest.ccmedia/mojo/services/mojo_renderer_service.cc
Patch
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);
Regression Test / PoC
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
Original Bug Report
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.ccchromecast/starboard/media/renderer/geometry_change_handler.ccchromecast/starboard/media/renderer/geometry_change_handler.hchromecast/starboard/media/renderer/starboard_player_manager.ccmedia/mojo/services/mojo_renderer_service.ccchromecast/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
MojoRendererService::Initialize(inmedia/mojo/services/mojo_renderer_service.cc) uses a debug-onlyDCHECK_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.- When
Initializeis called a second time, it eventually invokesStarboardRenderer::Initialize(inchromecast/starboard/media/renderer/starboard_renderer.cc). StarboardRenderer::Initializeattempts to create a newStarboardPlayerManagerby callingStarboardPlayerManager::Create(...).- If the attacker provides an unsupported media configuration (e.g., using
AudioCodec::kAMR_NB),Createfails and returnsnullptr. - The assignment
player_manager_ = StarboardPlayerManager::Create(...)evaluates tonullptr. Becauseplayer_manager_is astd::unique_ptr, this destroys the oldStarboardPlayerManagerfrom the first initialization, which in turn callsSbPlayerDestroyto free the underlying vendor-allocatedSbPlayer. - Because
player_manager_is now null,StarboardRenderer::Initializereturns early with an error:if (!player_manager_) { // Early return on failure media_task_runner_->PostTask(..., PIPELINE_ERROR_INITIALIZATION_FAILED); return; } - Crucially, the early return skips the subsequent call to
geometry_change_handler_.SetSbPlayer(...). As a result, theGeometryChangeHandlerretains a danglingraw_ptr<void> sb_player_pointing to the freed memory. - The attacker can then trigger
GeometryChangeHandler::OnVideoGeometryChangeby submitting a compositor frame from the renderer containing aVideoHoleDrawQuadwith the originaloverlay_plane_idand attacker-controlled bounds. OnVideoGeometryChangecallsSetPlayerBounds, which invokes the vendor Starboard APISbPlayerSetBoundswith the danglingsb_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.
- From a compromised renderer, call
media.mojom.InterfaceFactory::CreateCastRendererwith a generatedoverlay_plane_idto obtain a browser-processmedia.mojom.Rendererremote. - Call
Renderer::Initializewith a valid media configuration (e.g., AAC/H264) and wait for the success callback. This successfully allocates theSbPlayer. - Call
Renderer::Initializeagain on the same remote, this time providing an invalid configuration (e.g., advertisingAudioCodec::kAMR_NB). This frees theSbPlayerbut leaves a dangling pointer inGeometryChangeHandler. - Submit a compositor frame from the renderer containing a
VideoHoleDrawQuad. Set itsoverlay_plane_idto match the one from Step 1, and set its bounds to arbitrary values payload. - The Viz process will forward the bounds to the browser process, triggering a write to the freed
SbPlayermemory.
Suggested Fix
There are two places where this should be addressed for defense-in-depth:
- Prevent re-initialization at the Mojo boundary: In
MojoRendererService::Initialize, replace or supplement theDCHECKwith 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; } - Clear the dangling pointer on failure: In
StarboardRenderer::Initialize, ensure theGeometryChangeHandler’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.