diff --git a/gpu/BUILD.gn b/gpu/BUILD.gn index 2f916f0c3b31b..376524f26ac9d 100644 --- a/gpu/BUILD.gn +++ b/gpu/BUILD.gn @@ -509,6 +509,7 @@ test("gpu_unittests") { "config/gpu_test_expectations_parser_unittest.cc", "config/gpu_util_unittest.cc", "ipc/client/command_buffer_proxy_impl_unittest.cc", + "ipc/client/gpu_channel_host_observer_uaf_unittest.cc", "ipc/common/mojom_traits_unittest.cc", "ipc/host/gpu_disk_cache_unittest.cc", "ipc/service/gpu_channel_test_common.cc", diff --git a/gpu/ipc/client/gpu_channel_host.cc b/gpu/ipc/client/gpu_channel_host.cc index ef48c88e90814..9ca13ca7e8dcd 100644 --- a/gpu/ipc/client/gpu_channel_host.cc +++ b/gpu/ipc/client/gpu_channel_host.cc @@ -25,6 +25,7 @@ #include "url/gurl.h" #include "base/check_op.h" #include "base/check.h" +#include "base/threading/platform_thread.h" using base::AutoLock; @@ -391,6 +392,12 @@ GpuChannelHost::ConnectionTracker::~ConnectionTracker() { void GpuChannelHost::ConnectionTracker::OnDisconnectedFromGpuProcess() { is_connected_.store(false); + // POC: Widen the existing TOCTOU window between the lock-free + // is_connected_ store above and the lock-protected observer iteration + // below. In production this window is a few instructions wide; the sleep + // makes the cross-thread race deterministic for ASAN demonstration. It + // does not introduce any new ordering that production lacks. + base::PlatformThread::Sleep(base::Milliseconds(500)); NotifyGpuChannelLost(); } diff --git a/gpu/ipc/client/gpu_channel_host_observer_uaf_unittest.cc b/gpu/ipc/client/gpu_channel_host_observer_uaf_unittest.cc new file mode 100644 index 0000000000000..9e5020d37d586 --- /dev/null +++ b/gpu/ipc/client/gpu_channel_host_observer_uaf_unittest.cc @@ -0,0 +1,126 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// POC unit test for a TOCTOU race between +// GpuChannelHost::ConnectionTracker::OnDisconnectedFromGpuProcess (IO thread) +// and an observer destructor that follows the +// `if (!IsLost()) RemoveObserver(this)` pattern used by +// media::OOPVideoDecoderFactoryProcessService::~OOPVideoDecoderFactoryProcessService. +// +// The IO thread sets `is_connected_ = false` (lock-free) before taking +// `channel_obs_lock_` to iterate observers. A concurrent destructor on another +// thread can observe `IsLost() == true`, skip RemoveObserver(), and free the +// object while a raw pointer to it remains in `observer_list_`. The subsequent +// virtual call `observer->OnGpuChannelLost()` is a use-after-free. +// +// The race window is widened with an injected sleep (see the patch to +// gpu_channel_host.cc) so ASAN catches it deterministically. + +#include "base/memory/scoped_refptr.h" +#include "base/message_loop/message_pump_type.h" +#include "base/run_loop.h" +#include "base/test/scoped_feature_list.h" +#include "base/test/task_environment.h" +#include "base/threading/platform_thread.h" +#include "base/threading/thread.h" +#include "gpu/command_buffer/common/shared_image_capabilities.h" +#include "gpu/config/gpu_finch_features.h" +#include "gpu/ipc/client/gpu_channel_host.h" +#include "gpu/ipc/client/gpu_channel_observer.h" +#include "mojo/public/cpp/system/message_pipe.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gpu { +namespace { + +// Mirrors the registration/teardown pattern of +// media::OOPVideoDecoderFactoryProcessService: a heap-allocated +// GpuChannelLostObserver whose destructor only unregisters when the channel is +// not already reported lost. +class VictimObserver final : public GpuChannelLostObserver { + public: + explicit VictimObserver(scoped_refptr host) + : host_(std::move(host)) { + CHECK(host_->AddObserverIfNotAlreadyLost(this)); + } + + ~VictimObserver() { + // Same TOCTOU-unsafe pattern as ~OOPVideoDecoderFactoryProcessService: + // if (shared_image_interface_ && !shared_image_interface_->IsLost()) + // shared_image_interface_->RemoveGpuChannelLostObserver(this); + if (!host_->IsLost()) { + host_->RemoveObserver(this); + } + // |this| is freed on return while ConnectionTracker::observer_list_ may + // still hold a raw pointer to it. + } + + // GpuChannelLostObserver: + void OnGpuChannelLost() override { + // Called on the IO thread from ConnectionTracker::NotifyGpuChannelLost(). + // If the main thread already destroyed |this| via the path above, ASAN + // reports heap-use-after-free on the vtable load that dispatched here. + notified_ = true; + } + + private: + scoped_refptr host_; + volatile bool notified_ = false; +}; + +TEST(GpuChannelHostObserverUafTest, ToctouOnDisconnect) { + // Use the SharedRemote (non-legacy-IPC) code path so we can drive the + // disconnect handler with a bare mojo::MessagePipe. + base::test::ScopedFeatureList features; + features.InitAndEnableFeature(features::kRemoveGPULegacyIPC); + + base::test::TaskEnvironment task_env; + + // Dedicated IO thread, matching the utility-process layout (utility main + // thread vs utility IO thread). + base::Thread io_thread("PocIoThread"); + ASSERT_TRUE(io_thread.StartWithOptions( + base::Thread::Options(base::MessagePumpType::IO, 0))); + + // |handle0| goes to GpuChannelHost; |handle1| stands in for the GPU-process + // end. Closing |handle1| triggers ConnectionTracker:: + // OnDisconnectedFromGpuProcess on |io_thread|, exactly as in production. + mojo::MessagePipe pipe; + auto host = base::MakeRefCounted( + /*channel_id=*/0, GPUInfo(), GpuFeatureInfo(), SharedImageCapabilities(), + std::move(pipe.handle0), io_thread.task_runner()); + + // Register the heap-allocated observer (matches GetSharedImageInterface()'s + // call to AddObserverIfNotAlreadyLost(this)). + auto victim = std::make_unique(host); + + // Step 1 (IO thread): drop the GPU end. The SharedRemote disconnect handler + // posts to |io_thread| and runs OnDisconnectedFromGpuProcess(), which stores + // is_connected_=false and then (after the widened race window) walks the + // observer list under |channel_obs_lock_|. + pipe.handle1.reset(); + + // Step 2 (main thread): spin until the IO thread has published the lock-free + // "lost" state, i.e. we are now inside the TOCTOU window. + while (!host->IsLost()) { + base::PlatformThread::Sleep(base::Milliseconds(1)); + } + + // Step 3 (main thread): destroy the observer. Because IsLost() is true, the + // destructor skips RemoveObserver(this), leaving a dangling raw pointer in + // ConnectionTracker::observer_list_. + victim.reset(); + + // Step 4 (IO thread): NotifyGpuChannelLost() resumes and invokes + // observer->OnGpuChannelLost() on the freed object. ASAN: + // heap-use-after-free. + io_thread.FlushForTesting(); + io_thread.Stop(); + + host = nullptr; + base::RunLoop().RunUntilIdle(); +} + +} // namespace +} // namespace gpu