CVE-2026-106283
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/mirroring/service/mirroring_gpu_factories_factory.cc |
modified | |
GetInstancecomponents/mirroring/service/mirroring_gpu_factories_factory.cc |
modified | |
GpuChannelHostcomponents/mirroring/service/mirroring_gpu_factories_factory.h |
modified | |
ContextProviderCommandBuffercomponents/mirroring/service/mirroring_gpu_factories_factory.h |
modified | |
Gpucomponents/mirroring/service/mirroring_gpu_factories_factory.h |
modified | |
forcomponents/mirroring/service/openscreen_session_host.cc |
modified | |
ifcomponents/mirroring/service/openscreen_session_host.cc |
modified | |
io_thread_components/mirroring/service/mirroring_gpu_factories_factory_unittest.cc |
modified |
Files Changed
components/mirroring/service/mirroring_gpu_factories_factory.hcomponents/mirroring/service/BUILD.gncomponents/mirroring/service/openscreen_session_host.cccomponents/mirroring/service/mirroring_gpu_factories_factory_unittest.cccomponents/mirroring/service/mirroring_gpu_factories_factory.cccomponents/mirroring/service/DEPScomponents/mirroring/service/openscreen_session_host_unittest.cc
Patch
From 7a04d1516fcc845138d75809d2d38affcb32095c Mon Sep 17 00:00:00 2001 From: Hongchan Choi <[email protected]> Date: Mon, 14 Sep 2026 11:19:54 -0700 Subject: [PATCH] [Mirroring] Synchronize MirroringGpuFactoriesFactory initialization Update MirroringGpuFactoriesFactory to synchronize initialization and maintain a stable object lifetime across threading boundaries. Previously, lazy initialization and premature teardown upon GPU context loss could result in threading timing conflicts and invalid state access between the main and video encoding threads. MirroringGpuFactoriesFactory::Create() now synchronously checks GPU channel establishment on the main thread, returning null on failure to enable a graceful fallback to software encoding. Factory initialization is moved synchronously to construction, and context loss notifications no longer destroy the underlying accelerator instances prematurely. Add regression unit tests: - MirroringGpuFactoriesFactoryTest.MaintainsValidInstanceAcrossContextLoss - MirroringGpuFactoriesFactoryTest.HandlesBindFailureGracefully - MirroringGpuFactoriesFactoryTest.HandlesGpuChannelEstablishmentFailure - OpenscreenSessionHostTest.ShouldDisableHardwareEncodingIfGpuContextLost [email protected] Bug: 499468981 Test: components/mirroring:mirroring_unittests Change-Id: I2de140f9f83697ecf89cd3be6a6c03b4f39b69d8 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8393367 Reviewed-by: Jordan Bayles <[email protected]> Commit-Queue: Hongchan Choi <[email protected]> Reviewed-by: Hongchan Choi <[email protected]> Cr-Commit-Position: refs/heads/main@{#1697367} --- diff --git a/components/mirroring/service/mirroring_gpu_factories_factory.cc b/components/mirroring/service/mirroring_gpu_factories_factory.cc index 0ead202..0b579b5b 100644 --- a/components/mirroring/service/mirroring_gpu_factories_factory.cc +++ b/components/mirroring/service/mirroring_gpu_factories_factory.cc @@ -4,9 +4,12 @@ #include "components/mirroring/service/mirroring_gpu_factories_factory.h" +#include <utility> + #include "base/functional/bind.h" #include "base/functional/callback.h" #include "gpu/ipc/client/command_buffer_proxy_impl.h" +#include "gpu/ipc/client/gpu_channel_host.h" #include "media/mojo/mojom/video_encode_accelerator.mojom.h" #include "services/viz/public/cpp/gpu/command_buffer_metrics.h" #include "services/viz/public/cpp/gpu/context_provider_command_buffer.h" @@ -20,65 +23,55 @@ } -MirroringGpuFactoriesFactory::UniquePtr MirroringGpuFactoriesFactory::Create( +std::optional<MirroringGpuFactoriesFactory::UniquePtr> +MirroringGpuFactoriesFactory::Create( scoped_refptr<CastEnvironment> cast_environment, viz::Gpu& gpu, base::OnceClosure context_lost_cb, ContextConfiguredCallback context_configured_cb) { + CHECK(cast_environment->CurrentlyOn(CastEnvironment::ThreadId::kMain)); + + auto gpu_channel_host = gpu.EstablishGpuChannelSync(); + if (!gpu_channel_host) { + return std::nullopt; + } + + auto video_runner = + cast_environment->GetTaskRunner(CastEnvironment::ThreadId::kVideo); + + mojo::PendingRemote<media::mojom::VideoEncodeAcceleratorProvider> + vea_provider; + gpu.CreateVideoEncodeAcceleratorProvider( + vea_provider.InitWithNewPipeAndPassReceiver()); + return UniquePtr(new MirroringGpuFactoriesFactory( - cast_environment, gpu, std::move(context_lost_cb), + std::move(cast_environment), std::move(gpu_channel_host), + std::move(vea_provider), std::move(context_lost_cb), std::move(context_configured_cb)), - base::OnTaskRunnerDeleter(cast_environment->GetTaskRunner( - CastEnvironment::ThreadId::kVideo))); + base::OnTaskRunnerDeleter(std::move(video_runner))); } MirroringGpuFactoriesFactory::MirroringGpuFactoriesFactory( scoped_refptr<CastEnvironment> cast_environment, - viz::Gpu& gpu, + scoped_refptr<gpu::GpuChannelHost> gpu_channel_host, + mojo::PendingRemote<media::mojom::VideoEncodeAcceleratorProvider> + vea_provider, base::OnceClosure context_lost_cb, ContextConfiguredCallback context_configured_cb) : cast_environment_(std::move(cast_environment)), - gpu_(gpu), context_lost_cb_(std::move(context_lost_cb)), - context_configured_cb_(std::move(context_configured_cb)) {} - -MirroringGpuFactoriesFactory::~MirroringGpuFactoriesFactory() { - CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kVideo)); - ResetGpuFactories(); -} - -media::GpuVideoAcceleratorFactories& -MirroringGpuFactoriesFactory::GetInstance() { + context_configured_cb_(std::move(context_configured_cb)) { CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); + CHECK(gpu_channel_host); - // If we have a valid context, return the current instance as it is still - // valid. - if (instance_) { - return *instance_; - } - - // Finally, create and return a new instance. static constexpr int32_t kStreamId = 0; - auto gpu_channel_host = gpu_->EstablishGpuChannelSync(); - context_provider_ = viz::ContextProviderCommandBuffer::CreateForRaster( gpu_channel_host, kStreamId, gpu::SchedulingPriority::kHigh, GURL("chrome://gpu/CastStreaming"), /*automatic_flushes=*/false, /*support_locking=*/false, gpu::SharedMemoryLimits::ForMailboxContext(), - viz::command_buffer_metrics::ContextType::VIDEO_CAPTURE); - cast_environment_->PostTask( - CastEnvironment::ThreadId::kVideo, FROM_HERE, - base::BindOnce(&MirroringGpuFactoriesFactory::BindOnVideoThread, - weak_factory_.GetWeakPtr())); - - mojo::PendingRemote<media::mojom::VideoEncodeAcceleratorProvider> - vea_provider; - gpu_->CreateVideoEncodeAcceleratorProvider( - vea_provider.InitWithNewPipeAndPassReceiver()); - auto codec_factory = std::make_unique<media::MojoCodecFactoryDefault>( cast_environment_->GetTaskRunner(CastEnvironment::ThreadId::kVideo), context_provider_, @@ -95,6 +88,30 @@ /*enable_video_decode_accelerator=*/false, /*enable_video_encode_accelerator=*/true); + // NOTE: this Unretained is safe because deletion of `this` is posted to the + // VIDEO thread via base::OnTaskRunnerDeleter, so this task will always run + // before `this` is destroyed. + cast_environment_->PostTask( + CastEnvironment::ThreadId::kVideo, FROM_HERE, + base::BindOnce(&MirroringGpuFactoriesFactory::BindOnVideoThread, + base::Unretained(this))); +} + +MirroringGpuFactoriesFactory::~MirroringGpuFactoriesFactory() { + CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kVideo)); + if (context_provider_) { + context_provider_->RemoveObserver(this); + } + // Destroy the accelerator factories before releasing `context_provider_`, + // since `instance_` holds a reference to it. Done explicitly rather than + // relying on reverse member-declaration order. + instance_.reset(); +} + +media::GpuVideoAcceleratorFactories& +MirroringGpuFactoriesFactory::GetInstance() { + CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); + CHECK(instance_); return *instance_; } @@ -112,7 +129,7 @@ if (command_buffer_proxy) { command_buffer_proxy->GetGpuChannel().GetChannelToken(base::BindOnce( &MirroringGpuFactoriesFactory::OnChannelTokenReady, - weak_factory_.GetWeakPtr(), command_buffer_proxy->route_id())); + video_weak_factory_.GetWeakPtr(), command_buffer_proxy->route_id())); } } @@ -130,7 +147,6 @@ void MirroringGpuFactoriesFactory::OnContextLost() { CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kVideo)); - ResetGpuFactories(); if (context_lost_cb_) { // `context_lost_cb_` may destroy `this`, so it is important that it is // called last in this method. @@ -138,15 +154,4 @@ } } -void MirroringGpuFactoriesFactory::ResetGpuFactories() { - // The GPU factories object, after construction, must only be accessed on the - // video encoding thread (including for deletion). - CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kVideo)); - instance_.reset(); - if (context_provider_) { - context_provider_->RemoveObserver(this); - context_provider_ = nullptr; - } -} - } // namespace mirroring diff --git a/components/mirroring/service/mirroring_gpu_factories_factory.h b/components/mirroring/service/mirroring_gpu_factories_factory.h index fb887a36..c7f1ade 100644 --- a/components/mirroring/service/mirroring_gpu_factories_factory.h +++ b/components/mirroring/service/mirroring_gpu_factories_factory.h @@ -6,10 +6,12 @@ #define COMPONENTS_MIRRORING_SERVICE_MIRRORING_GPU_FACTORIES_FACTORY_H_ #include <memory> +#include <optional> #include "base/component_export.h" #include "base/functional/callback.h" -#include "base/memory/raw_ref.h" +#include "base/memory/scoped_refptr.h" +#include "base/memory/weak_ptr.h" #include "base/task/sequenced_task_runner.h" #include "base/unguessable_token.h" #include "components/viz/common/gpu/context_lost_observer.h" @@ -17,9 +19,14 @@ #include "media/mojo/clients/mojo_gpu_video_accelerator_factories.h" #include "media/video/gpu_video_accelerator_factories.h" +namespace gpu { +class GpuChannelHost; +} // namespace gpu + namespace viz { +class ContextProviderCommandBuffer; class Gpu; -} +} // namespace viz namespace mirroring { @@ -44,8 +51,10 @@ std::unique_ptr<MirroringGpuFactoriesFactory, base::OnTaskRunnerDeleter>; // Factory method to ensure the object is always managed by a UniquePtr - // that deletes it on the correct thread. - static UniquePtr Create( + // that deletes it on the correct thread. Must be called on the MAIN thread. + // Returns `std::nullopt` if the GPU channel cannot be established, in which + // case the caller should fall back to software encoding. + static std::optional<UniquePtr> Create( scoped_refptr<media::cast::CastEnvironment> cast_environment, viz::Gpu& gpu, base::OnceClosure context_lost_cb, @@ -64,7 +73,9 @@ private: MirroringGpuFactoriesFactory( scoped_refptr<media::cast::CastEnvironment> cast_environment, - viz::Gpu& gpu, + scoped_refptr<gpu::GpuChannelHost> gpu_channel_host, + mojo::PendingRemote<media::mojom::VideoEncodeAcceleratorProvider> + vea_provider, base::OnceClosure context_lost_cb, ContextConfiguredCallback context_configured_cb); @@ -74,17 +85,14 @@ void OnChannelTokenReady(int32_t route_id, const base::UnguessableToken& channel_token); - // Properly resets `instance_` and `context_provider_` on the VIDEO thread. - void ResetGpuFactories(); - scoped_refptr<media::cast::CastEnvironment> cast_environment_; - raw_ref<viz::Gpu, DisableDanglingPtrDetection> gpu_; scoped_refptr<viz::ContextProviderCommandBuffer> context_provider_; base::OnceClosure context_lost_cb_; ContextConfiguredCallback context_configured_cb_; std::unique_ptr<media::MojoGpuVideoAcceleratorFactories> instance_; - base::WeakPtrFactory<MirroringGpuFactoriesFactory> weak_factory_{this}; + // Weak pointer factory used exclusively on the VIDEO thread. + base::WeakPtrFactory<MirroringGpuFactoriesFactory> video_weak_factory_{this}; }; } // namespace mirroring diff --git a/components/mirroring/service/openscreen_session_host.cc b/components/mirroring/service/openscreen_session_host.cc index c1aedbe..bf0350d71 100644 --- a/components/mirroring/service/openscreen_session_host.cc +++ b/components/mirroring/service/openscreen_session_host.cc @@ -385,7 +385,13 @@ base::SingleThreadTaskRunner::GetCurrentDefault(), base::BindOnce(&OpenscreenSessionHost::OnGpuFactoriesConfigured, weak_factory_.GetWeakPtr()))); - gpu_factories = &(gpu_factories_factory_.value()->GetInstance()); + if (gpu_factories_factory_.has_value()) { + gpu_factories = &(gpu_factories_factory_.value()->GetInstance()); + } else { + // The GPU channel could not be established, so fall back to software + // encoding. + video_config->use_hardware_encoder = false; + } } auto video_encoder = media::cast::VideoEncoder::Create( @@ -781,6 +787,22 @@ base::StrCat({"stopped streaming. state=", base::NumberToString(static_cast<int>(state_))})); + // This cleanup must happen even when there is no active `session_`, since + // callers awaiting a VEA need an explicit failure rather than a dropped + // callback. + gpu_factories_factory_.reset(); + channel_token_ = base::UnguessableToken(); + route_id_ = 0; + + // Fail all pending VEA requests as the GPU factory is gone. This explicitly + // signals failure to callers, so they won't get an invalid token/ID. They'll + // simply know VEA creation failed. + for (auto& callback : pending_vea_requests_) { + std::move(callback).Run(base::SingleThreadTaskRunner::GetCurrentDefault(), + nullptr); + } + pending_vea_requests_.clear(); + if (!session_) { return; } @@ -790,7 +812,6 @@ audio_sender_.reset(); video_sender_.reset(); refresh_timer_.Stop(); - gpu_factories_factory_.reset(); remoting_stream_data_.reset(); } @@ -961,22 +982,10 @@ CHECK(config.use_hardware_encoder); CHECK_EQ(state_, State::kMirroring); - gpu_factories_factory_.reset(); - channel_token_ = base::UnguessableToken(); - route_id_ = 0; base::UmaHistogramEnumeration( "MediaRouter.MirroringService.GpuFactoryContextLost", config.video_codec()); - // Fail all pending VEA requests as the GPU factory is lost. This explicitly - // signals failure to callers, so they won't get an invalid token/ID. They'll - // simply know VEA creation failed. - for (auto& callback : pending_vea_requests_) { - std::move(callback).Run(base::SingleThreadTaskRunner::GetCurrentDefault(), - nullptr); - } - pending_vea_requests_.clear(); - MaybeDenylistHardwareCodecAndRenegotiate(config.video_codec()); } diff --git a/components/mirroring/service/mirroring_gpu_factories_factory_unittest.cc b/components/mirroring/service/mirroring_gpu_factories_factory_unittest.cc index 357c03ef..1fca96f 100644 --- a/components/mirroring/service/mirroring_gpu_factories_factory_unittest.cc +++ b/components/mirroring/service/mirroring_gpu_factories_factory_unittest.cc @@ -4,18 +4,28 @@ #include "components/mirroring/service/mirroring_gpu_factories_factory.h" +#include <atomic> +#include <memory> +#include <utility> + #include "base/functional/bind.h" #include "base/functional/callback_helpers.h" #include "base/memory/scoped_refptr.h" #include "base/run_loop.h" #include "base/task/thread_pool.h" #include "base/test/task_environment.h" +#include "base/threading/thread.h" #include "base/time/default_tick_clock.h" +#include "gpu/command_buffer/common/shared_image_capabilities.h" +#include "gpu/config/gpu_feature_info.h" +#include "gpu/config/gpu_info.h" #include "media/cast/cast_environment.h" #include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/system/message_pipe.h" #include "services/viz/public/cpp/gpu/gpu.h" #include "services/viz/public/mojom/gpu.mojom.h" #include "testing/gtest/include/gtest/gtest.h" +#include "ui/gl/gl_implementation.h" namespace mirroring { @@ -24,11 +34,15 @@ MirroringGpuFactoriesFactoryTest() : task_environment_( base::test::TaskEnvironment::MainThreadType::UI, - base::test::TaskEnvironment::ThreadPoolExecutionMode::ASYNC) { + base::test::TaskEnvironment::ThreadPoolExecutionMode::ASYNC), + io_thread_("TestIO") { main_task_runner_ = task_environment_.GetMainThreadTaskRunner(); video_task_runner_ = base::ThreadPool::CreateSingleThreadTaskRunner({base::MayBlock()}); ... (file diff truncated)
Regression Test / PoC
diff --git a/components/mirroring/service/mirroring_gpu_factories_factory_unittest.cc b/components/mirroring/service/mirroring_gpu_factories_factory_unittest.cc
index 357c03ef..1fca96f 100644
--- a/components/mirroring/service/mirroring_gpu_factories_factory_unittest.cc
+++ b/components/mirroring/service/mirroring_gpu_factories_factory_unittest.cc
@@ -4,18 +4,28 @@
#include "components/mirroring/service/mirroring_gpu_factories_factory.h"
+#include <atomic>
+#include <memory>
+#include <utility>
+
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/task/thread_pool.h"
#include "base/test/task_environment.h"
+#include "base/threading/thread.h"
#include "base/time/default_tick_clock.h"
+#include "gpu/command_buffer/common/shared_image_capabilities.h"
+#include "gpu/config/gpu_feature_info.h"
+#include "gpu/config/gpu_info.h"
#include "media/cast/cast_environment.h"
#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/system/message_pipe.h"
#include "services/viz/public/cpp/gpu/gpu.h"
#include "services/viz/public/mojom/gpu.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gl/gl_implementation.h"
namespace mirroring {
@@ -24,11 +34,15 @@
MirroringGpuFactoriesFactoryTest()
: task_environment_(
base::test::TaskEnvironment::MainThreadType::UI,
- base::test::TaskEnvironment::ThreadPoolExecutionMode::ASYNC) {
+ base::test::TaskEnvironment::ThreadPoolExecutionMode::ASYNC),
+ io_thread_("TestIO") {
main_task_runner_ = task_environment_.GetMainThreadTaskRunner();
video_task_runner_ =
base::ThreadPool::CreateSingleThreadTaskRunner({base::MayBlock()});
+ CHECK(io_thread_.StartWithOptions(
+ base::Thread::Options(base::MessagePumpType::IO, 0)));
+
cast_environment_ = base::MakeRefCounted<media::cast::CastEnvironment>(
*base::DefaultTickClock::GetInstance(), main_task_runner_,
main_task_runner_, // audio
@@ -38,9 +52,11 @@
base::Unretained(this)));
mojo::PendingRemote<viz::mojom::Gpu> gpu_remote;
- // Bind the receiver to avoid disconnects during the test.
- gpu_receiver_.Bind(gpu_remote.InitWithNewPipeAndPassReceiver());
- gpu_ = viz::Gpu::Create(std::move(gpu_remote), main_task_runner_);
+ io_thread_.task_runner()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&StubGpu::BindReceiver, base::Unretained(&stub_gpu_),
+ gpu_remote.InitWithNewPipeAndPassReceiver()));
+ gpu_ = viz::Gpu::Create(std::move(gpu_remote), io_thread_.task_runner());
}
void TearDown() override {
@@ -59,6 +75,15 @@
base::RunLoop run_loop;
environment_destruction_closure_ = run_loop.QuitClosure();
run_loop.Run();
+
+ base::RunLoop io_loop;
+ io_thread_.task_runner()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&StubGpu::ResetReceiver, base::Unretained(&stub_gpu_))
+ .Then(io_loop.QuitClosure()));
+ io_loop.Run();
+
+ io_thread_.Stop();
}
void OnCastEnvironmentDestroyed() {
@@ -69,20 +94,43 @@
protected:
base::test::TaskEnvironment task_environment_;
+ base::Thread io_thread_;
scoped_refptr<base::SingleThreadTaskRunner> main_task_runner_;
scoped_refptr<base::SingleThreadTaskRunner> video_task_runner_;
scoped_refptr<media::cast::CastEnvironment> cast_environment_;
std::unique_ptr<viz::Gpu> gpu_;
base::OnceClosure environment_destruction_closure_;
- private:
// Dummy receiver to keep the mojo connection alive.
class StubGpu : public viz::mojom::Gpu {
public:
StubGpu() = default;
~StubGpu() override = default;
- void EstablishGpuChannel(EstablishGpuChannelCallback callback) override {}
+ void BindReceiver(mojo::PendingReceiver<viz::mojom::Gpu> receiver) {
+ receiver_.Bind(std::move(receiver));
+ }
+
+ void ResetReceiver() { receiver_.reset(); }
+
+ void EstablishGpuChannel(EstablishGpuChannelCallback callback) override {
+ if (!channel_handle_valid_) {
+ std::move(callback).Run(
+ /*client_id=*/0, mojo::ScopedMessagePipeHandle(), gpu::GPUInfo(),
+ gpu::GpuFeatureInfo(), gpu::SharedImageCapabilities());
+ return;
+ }
+ mojo::MessagePipe pipe;
+ held_channel_handle_ = std::move(pipe.handle1);
+ gpu::GPUInfo info;
+ info.gl_implementation_parts.gl = gl_disabled_
+ ? gl::kGLImplementationDisabled
+ : gl::kGLImplementationEGLGLES2;
+ std::move(callback).Run(/*client_id=*/1, std::move(pipe.handle0), info,
+ gpu::GpuFeatureInfo(),
+ gpu::SharedImageCapabilities());
+ }
+
void CreateVideoEncodeAcceleratorProvider(
mojo::PendingReceiver<media::mojom::VideoEncodeAcceleratorProvider>
receiver) override {}
@@ -91,14 +139,23 @@
mojo::PendingReceiver<chromeos_camera::mojom::MjpegDecodeAccelerator>
jda_receiver) override {}
#endif
+
+ void set_gl_disabled(bool disabled) { gl_disabled_ = disabled; }
+ void set_channel_handle_valid(bool valid) { channel_handle_valid_ = valid; }
+
+ private:
+ std::atomic<bool> gl_disabled_{true};
+ std::atomic<bool> channel_handle_valid_{true};
+ mojo::ScopedMessagePipeHandle held_channel_handle_;
+ mojo::Receiver<viz::mojom::Gpu> receiver_{this};
};
StubGpu stub_gpu_;
- mojo::Receiver<viz::mojom::Gpu> gpu_receiver_{&stub_gpu_};
};
TEST_F(MirroringGpuFactoriesFactoryTest, DestroysOnVideoThread) {
auto factory = MirroringGpuFactoriesFactory::Create(
cast_environment_, *gpu_, base::DoNothing(), base::DoNothing());
+ ASSERT_TRUE(factory.has_value());
// Resetting on the main thread should trigger a task on the video thread.
factory.reset();
@@ -116,6 +173,7 @@
cast_environment_, *gpu_,
base::BindOnce([](bool* called) { *called = true; }, &lost_called),
base::DoNothing());
+ ASSERT_TRUE(factory.has_value());
video_task_runner_->PostTask(FROM_HERE,
base::BindOnce(
@@ -124,14 +182,13 @@
factory->OnContextLost();
std::move(quit_closure).Run();
},
- factory.get(), run_loop.QuitClosure()));
+ factory->get(), run_loop.QuitClosure()));
run_loop.Run();
EXPECT_TRUE(lost_called);
- // The factory should have been reset in OnContextLost if it triggers
- // its own destruction (which OpenscreenSessionHost does). In this test,
- // we just manually reset it.
+ // Teardown is deferred until the owner drops its reference to the factory,
+ // which destroys the factory on the video thread.
factory.reset();
base::RunLoop cleanup_loop;
@@ -139,4 +196,86 @@
cleanup_loop.Run();
}
+TEST_F(MirroringGpuFactoriesFactoryTest,
+ MaintainsValidInstanceAcrossContextLoss) {
+ base::RunLoop run_loop;
+ bool lost_called = false;
+
+ auto factory = MirroringGpuFactoriesFactory::Create(
+ cast_environment_, *gpu_,
+ base::BindOnce([](bool* called) { *called = true; }, &lost_called),
+ base::DoNothing());
+ ASSERT_TRUE(factory.has_value());
+
+ media::GpuVideoAcceleratorFactories& factories = (*factory)->GetInstance();
+
+ video_task_runner_->PostTask(FROM_HERE,
+ base::BindOnce(
+ [](MirroringGpuFactoriesFactory* factory,
+ base::OnceClosure quit_closure) {
+ factory->OnContextLost();
+ std::move(quit_closure).Run();
+ },
+ factory->get(), run_loop.QuitClosure()));
+ run_loop.Run();
+
+ EXPECT_TRUE(lost_called);
+
+ // The factories reference must remain valid and callable after context loss.
+ EXPECT_TRUE(factories.IsGpuVideoEncodeAcceleratorEnabled());
+ EXPECT_EQ(factories.GetTaskRunner(), video_task_runner_);
+
+ factory.reset();
+
+ base::RunLoop cleanup_loop;
+ video_task_runner_->PostTask(FROM_HERE, cleanup_loop.QuitClosure());
+ cleanup_loop.Run();
+}
+
+TEST_F(MirroringGpuFactoriesFactoryTest, HandlesBindFailureGracefully) {
+ base::RunLoop run_loop;
+ bool lost_called = false;
+
+ stub_gpu_.set_gl_disabled(true);
+
+ auto factory = MirroringGpuFactoriesFactory::Create(
+ cast_environment_, *gpu_,
+ base::BindOnce(
+ [](bool* called, base::OnceClosure quit) {
+ *called = true;
+ std::move(quit).Run();
+ },
+ &lost_called, run_loop.QuitClosure()),
+ base::DoNothing());
+ ASSERT_TRUE(factory.has_value());
+
+ media::GpuVideoAcceleratorFactories& factories = (*factory)->GetInstance();
+
+ // Wait for the video thread to attempt binding and trigger context lost
+ // callback.
+ run_loop.Run();
+
+ EXPECT_TRUE(lost_called);
+
+ // The factories reference must remain valid and safe to query even after bind
+ // failure.
+ EXPECT_TRUE(factories.IsGpuVideoEncodeAcceleratorEnabled());
+ EXPECT_EQ(factories.GetTaskRunner(), video_task_runner_);
+
+ factory.reset();
+
+ base::RunLoop cleanup_loop;
+ video_task_runner_->PostTask(FROM_HERE, cleanup_loop.QuitClosure());
+ cleanup_loop.Run();
+}
+
+TEST_F(MirroringGpuFactoriesFactoryTest,
+ HandlesGpuChannelEstablishmentFailure) {
+ stub_gpu_.set_channel_handle_valid(false);
+
+ auto factory = MirroringGpuFactoriesFactory::Create(
+ cast_environment_, *gpu_, base::DoNothing(), base::DoNothing());
+ EXPECT_FALSE(factory.has_value());
+}
+
} // namespace mirroring
diff --git a/components/mirroring/service/openscreen_session_host_unittest.cc b/components/mirroring/service/openscreen_session_host_unittest.cc
index d60fcdd..faf1451e 100644
--- a/components/mirroring/service/openscreen_session_host_unittest.cc
+++ b/components/mirroring/service/openscreen_session_host_unittest.cc
@@ -185,6 +185,10 @@
OpenscreenSessionHostTest& operator=(const OpenscreenSessionHostTest&) =
delete;
+ void SetUp() override {
+ media::cast::encoding_support::ClearHardwareCodecDenyListForTesting();
+ }
+
void TearDown() override {
media::cast::encoding_support::ClearHardwareCodecDenyListForTesting();
}
@@ -201,6 +205,7 @@
if (session_host_) {
DeleteSessionHost();
}
+ media::cast::encoding_support::ClearHardwareCodecDenyListForTesting();
}
protected:
@@ -703,6 +708,10 @@
session_host_->video_sender_ = std::move(video_sender);
}
+ void PushGpuFactoryContextLost(const media::cast::FrameSenderConfig& config) {
+ session_host_->OnGpuFactoryContextLost(config);
+ }
+
uint32_t GetVideoNetworkBandwidth() const {
return session_host_->GetVideoNetworkBandwidth();
}
@@ -1193,6 +1202,39 @@
... (truncated)
Original Bug Report
Use-After-Free via Data Race in MirroringGpuFactoriesFactory
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 without the security team.
Overview: A data race exists in MirroringGpuFactoriesFactory where the Main and Video threads concurrently modify a scoped_refptr without synchronization. This race can be triggered during a GPU context loss, resulting in a Use-After-Free (UAF) of the GPU context provider. Because scoped_refptr uses a raw pointer internally, this UAF completely bypasses MiraclePtr protections.
Affected files:
components/mirroring/service/mirroring_gpu_factories_factory.cccomponents/mirroring/service/mirroring_gpu_factories_factory.hcomponents/mirroring/service/openscreen_session_host.cc
Estimated timestamp from git blame: 2026-02-27
Summary
A critical thread-safety vulnerability exists in MirroringGpuFactoriesFactory within the Mirroring Service (utility process). The class shares state between the Main thread and the Video encoding thread without explicit synchronization. A data race on a scoped_refptr can result in a Use-After-Free (UAF) condition that explicitly bypasses MiraclePtr (BackupRefPtr) protections, potentially leading to Remote Code Execution (RCE).
Vulnerability Details
In MirroringGpuFactoriesFactory, two member variables are accessed concurrently by different threads:
context_provider_(ascoped_refptr<viz::ContextProviderCommandBuffer>)instance_(astd::unique_ptr<media::MojoGpuVideoAcceleratorFactories>)
During initialization, GetInstance() runs on the Main thread (kMain). It instantiates the context_provider_ and then posts a task (BindOnVideoThread) to the Video thread (kVideo). Because PostTask is non-blocking, GetInstance() immediately continues executing on the Main thread.
If the GPU context is lost or the bind fails, the Video thread immediately executes OnContextLost(), which calls ResetGpuFactories(). Inside ResetGpuFactories(), the Video thread clears both members:
instance_.reset();
if (context_provider_) {
context_provider_->RemoveObserver(this);
context_provider_ = nullptr; // Data race here!
}
Meanwhile, the Main thread is still inside GetInstance(), attempting to pass the context_provider_ by value into the MojoCodecFactoryDefault constructor. To do this, it invokes the scoped_refptr copy constructor, which reads the internal raw pointer.
Because there is no base::Lock or sequence checking protecting these members, a C++ data race occurs. The Video thread can nullify the scoped_refptr and destroy the underlying object at the exact moment the Main thread is reading the internal raw pointer to increment its reference count. The Main thread will end up wrapping a dangling pointer, creating a Use-After-Free.
MiraclePtr Bypass
This vulnerability bypasses MiraclePtr (BackupRefPtr). Chromium’s scoped_refptr implementation uses a raw C++ pointer internally (T* ptr_) which is explicitly annotated with RAW_PTR_EXCLUSION. Because the data race occurs directly on the scoped_refptr’s internal pointer, the memory access is not guarded by MiraclePtr quarantine checks.
Potential Steps to Trigger
Note: These are suggested steps. Our tooling agent does not yet have the ability to run code and provide a working proof-of-concept.
- An attacker compromises a renderer process or uses malicious Web APIs (e.g., Presentation API) to initiate a mirroring session.
- The browser process launches the Mirroring Service utility process, creating an
OpenscreenSessionHost. - The host requests hardware encoding, invoking
MirroringGpuFactoriesFactory::GetInstance()on the Main thread. GetInstance()creates the context provider and posts the binding task to the Video thread.- The attacker intentionally triggers a GPU context loss (e.g., by exhausting GPU memory or sending malformed WebGL commands from the compromised renderer).
- The context loss forces the Video thread to immediately fail the bind operation and call
ResetGpuFactories(), destroying the context provider. - Simultaneously, the Main thread attempts to copy the
scoped_refptrto initialize the codec factory, catching the exact race window where the object is being freed. - The Main thread passes the dangling pointer to the video encoder, triggering the UAF when the encoder later attempts to use the freed context provider.
Suggested Fix
Shared members in MirroringGpuFactoriesFactory (specifically context_provider_ and instance_) must be protected.
- Option 1: Introduce a
base::Lockto synchronize all read and write access tocontext_provider_andinstance_across the Main and Video threads. - Option 2: Refactor the initialization sequence so that all creation, access, and destruction of these GPU factory members happens strictly on a single sequence (e.g., the Video thread), returning a
SafeRefor thread-safe interface to the Main thread.
Evaluated with Chrome root at commit: 09ec9e7cc4d24823d20b6d37cf3d282734f6bf0f
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.