diff --git a/ui/base/clipboard/BUILD.gn b/ui/base/clipboard/BUILD.gn --- a/ui/base/clipboard/BUILD.gn +++ b/ui/base/clipboard/BUILD.gn @@ -294,7 +294,10 @@ source_set("clipboard_test") { } if (is_win) { - sources += [ "clipboard_util_win_unittest.cc" ] + sources += [ + "clipboard_format_type_win_race_unittest.cc", + "clipboard_util_win_unittest.cc", + ] } if (is_android) { diff --git a/ui/base/clipboard/clipboard_format_type_win_race_unittest.cc b/ui/base/clipboard/clipboard_format_type_win_race_unittest.cc new file mode 100644 --- /dev/null +++ b/ui/base/clipboard/clipboard_format_type_win_race_unittest.cc @@ -0,0 +1,117 @@ +// 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 unsynchronized mutation of the static std::map in +// ClipboardFormatType::FileContentAtIndexType(). +// +// FileContentTypeMap() returns a process-global static +// base::NoDestructor>. Every call to +// FileContentAtIndexType(index) performs an std::map::insert on that container +// with no lock. In production this is reached concurrently from: +// - base::ThreadPool workers running ExtractVirtualFiles() +// (clipboard_util_win.cc:930), which is unsequenced, and +// - the UI thread via HasVirtualFilenames / SetFileContents. +// +// This test reproduces the race directly: N threads simultaneously insert +// fresh indices into the shared map. Under is_asan=true on Windows the +// corrupted red-black tree eventually yields heap-use-after-free / +// heap-buffer-overflow inside the libc++ __tree node manipulation; under a +// hypothetical Windows TSAN it reports a data race on the first iteration. + +#include +#include +#include + +#include "base/synchronization/waitable_event.h" +#include "base/threading/simple_thread.h" +#include "build/build_config.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/clipboard/clipboard_format_type.h" + +#if !BUILDFLAG(IS_WIN) +#error "This POC targets Windows-only code (clipboard_format_type_win.cc)." +#endif + +namespace ui { +namespace { + +constexpr int kNumThreads = 8; +// Large enough that every thread is allocating/rebalancing fresh RB-tree +// nodes for most of the run, and that ASAN has a realistic chance of +// observing the resulting corruption. +constexpr LONG kIndicesPerThread = 5000; + +class MapHammerer : public base::DelegateSimpleThread::Delegate { + public: + MapHammerer(int thread_idx, + base::WaitableEvent* start_gate, + std::atomic* ready_count) + : thread_idx_(thread_idx), + start_gate_(start_gate), + ready_count_(ready_count) {} + + void Run() override { + // Rendezvous so all threads enter the hot loop at the same instant. + ready_count_->fetch_add(1, std::memory_order_acq_rel); + start_gate_->Wait(); + + // Each thread inserts a disjoint half and an overlapping half so we get + // both "two writers allocate the same key" and "writer rebalances while + // another writer links a sibling" interleavings. + const LONG base = static_cast(thread_idx_) * kIndicesPerThread; + for (LONG i = 0; i < kIndicesPerThread; ++i) { + // Disjoint key — guaranteed fresh node allocation + tree rebalance. + (void)ClipboardFormatType::FileContentAtIndexType(base + i); + // Shared key range — multiple threads race to insert the same node. + (void)ClipboardFormatType::FileContentAtIndexType(i); + } + } + + private: + const int thread_idx_; + base::WaitableEvent* const start_gate_; + std::atomic* const ready_count_; +}; + +} // namespace + +// Note: this test mutates process-global state (the NoDestructor map). It is +// intended to be run in isolation via --gtest_filter. +TEST(ClipboardFormatTypeWinRaceTest, ConcurrentFileContentAtIndexType) { + base::WaitableEvent start_gate( + base::WaitableEvent::ResetPolicy::MANUAL, + base::WaitableEvent::InitialState::NOT_SIGNALED); + std::atomic ready_count{0}; + + std::vector> delegates; + std::vector> threads; + for (int t = 0; t < kNumThreads; ++t) { + delegates.push_back( + std::make_unique(t, &start_gate, &ready_count)); + threads.push_back(std::make_unique( + delegates.back().get(), "clipboard_race")); + threads.back()->Start(); + } + + // Spin until every worker is parked on the gate, then release them all. + while (ready_count.load(std::memory_order_acquire) < kNumThreads) { + } + start_gate.Signal(); + + for (auto& thread : threads) { + thread->Join(); + } + + // If we survived the concurrent inserts, walk the (possibly corrupted) + // tree. With a torn RB-tree this is where ASAN typically fires + // heap-use-after-free on a dangling __tree_node pointer. + volatile size_t sink = 0; + for (LONG i = 0; i < kNumThreads * kIndicesPerThread; ++i) { + sink += static_cast( + ClipboardFormatType::FileContentAtIndexType(i).ToFormatEtc().lindex); + } + (void)sink; +} + +} // namespace ui