CVE-2026-106238
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
cache_directory_content/browser/font_unique_name_lookup/font_unique_name_lookup_android.cc |
modified | |
forcontent/browser/font_unique_name_lookup/font_unique_name_lookup_android.cc |
modified | |
TestFontUniqueNameLookupcontent/browser/font_unique_name_lookup/font_unique_name_lookup_android_unittest.cc |
modified | |
FontUniqueNameLookupCallbackTestcontent/browser/font_unique_name_lookup/font_unique_name_lookup_android_unittest.cc |
modified | |
TEST_Fcontent/browser/font_unique_name_lookup/font_unique_name_lookup_android_unittest.cc |
modified | |
forcontent/browser/font_unique_name_lookup/font_unique_name_lookup_android_unittest.cc |
modified | |
BindLambdaForTestingcontent/browser/font_unique_name_lookup/font_unique_name_lookup_android_unittest.cc |
modified |
Files Changed
content/browser/font_unique_name_lookup/font_unique_name_lookup_android.cccontent/browser/font_unique_name_lookup/font_unique_name_lookup_android_unittest.cccontent/browser/font_unique_name_lookup/font_unique_name_lookup_android.h
Patch
From 7bcc0ec5dd38106192174dc2ad9f59d4217158b5 Mon Sep 17 00:00:00 2001 From: Dominik Röttsches <[email protected]> Date: Mon, 14 Sep 2026 02:50:38 -0700 Subject: [PATCH] Sequentialize access to callbacks in FontUniqueNameLookup Serialize callback registrations, iterations, and drains onto a dedicated base::SequencedTaskRunner (callback_access_runner_) to eliminate race conditions around access to pending_callbacks_. Add FontUniqueNameLookupCallbackTest.ConcurrentCallbackQueuing to verify safe concurrent callback queuing and dispatch across ThreadPool tasks while asynchronous table generation and drainage are in progress. Bug: 492374387 Change-Id: Iee5e0d761bf82c38df5d0d0d14945f6ced3e912a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8379663 Commit-Queue: Dominik Röttsches <[email protected]> Reviewed-by: Francois Pierre Doray <[email protected]> Cr-Commit-Position: refs/heads/main@{#1697142} --- diff --git a/content/browser/font_unique_name_lookup/font_unique_name_lookup_android.cc b/content/browser/font_unique_name_lookup/font_unique_name_lookup_android.cc index f356b3b..d87bb37 100644 --- a/content/browser/font_unique_name_lookup/font_unique_name_lookup_android.cc +++ b/content/browser/font_unique_name_lookup/font_unique_name_lookup_android.cc @@ -124,7 +124,13 @@ FontUniqueNameLookup::FontUniqueNameLookup( const base::FilePath& cache_directory) - : cache_directory_(cache_directory) { + : callback_access_runner_(base::ThreadPool::CreateSequencedTaskRunner( + {base::TaskPriority::USER_BLOCKING, + base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})), + cache_directory_(cache_directory) { + CHECK(callback_access_runner_); + DETACH_FROM_SEQUENCE(callback_sequence_checker_); + if (!DirectoryExists(cache_directory_) || !base::PathIsWritable(cache_directory_)) { DCHECK(false) << "Error accessing cache directory for writing: " @@ -145,6 +151,32 @@ scoped_refptr<base::SequencedTaskRunner> task_runner, blink::mojom::FontUniqueNameLookup::GetUniqueNameLookupTableCallback callback) { + if (proto_storage_ready_.IsSignaled()) { + task_runner->PostTask(FROM_HERE, base::BindOnce(std::move(callback), + DuplicateMemoryRegion())); + return; + } + + CHECK(callback_access_runner_); + callback_access_runner_->PostTask( + FROM_HERE, + base::BindOnce(&FontUniqueNameLookup::EnqueueCallbackOnSequence, + base::Unretained(this), std::move(task_runner), + std::move(callback))); +} + +void FontUniqueNameLookup::EnqueueCallbackOnSequence( + scoped_refptr<base::SequencedTaskRunner> task_runner, + blink::mojom::FontUniqueNameLookup::GetUniqueNameLookupTableCallback + callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(callback_sequence_checker_); + + if (proto_storage_ready_.IsSignaled()) { + task_runner->PostTask(FROM_HERE, base::BindOnce(std::move(callback), + DuplicateMemoryRegion())); + return; + } + pending_callbacks_.emplace_back(std::move(task_runner), std::move(callback)); } @@ -210,6 +242,7 @@ } void FontUniqueNameLookup::ScheduleLoadOrUpdateTable() { + CHECK(callback_access_runner_); base::ThreadPool::PostTask( FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT, @@ -229,7 +262,11 @@ instance->PersistToFile(); } instance->proto_storage_ready_.Signal(); - instance->PostCallbacks(); + // Safe to use base::Unretained(instance) here because instance + // is a never-deleted singleton. + instance->callback_access_runner_->PostTask( + FROM_HERE, base::BindOnce(&FontUniqueNameLookup::PostCallbacks, + base::Unretained(instance))); }, base::Unretained(this))); } @@ -283,6 +320,9 @@ FontUniqueNameLookup::CallbackOnTaskRunner::~CallbackOnTaskRunner() = default; void FontUniqueNameLookup::PostCallbacks() { + DCHECK_CALLED_ON_VALID_SEQUENCE(callback_sequence_checker_); + CHECK(proto_storage_ready_.IsSignaled()); + for (auto& pending_callback : pending_callbacks_) { pending_callback.task_runner->PostTask( FROM_HERE, base::BindOnce(std::move(pending_callback.mojo_callback), diff --git a/content/browser/font_unique_name_lookup/font_unique_name_lookup_android.h b/content/browser/font_unique_name_lookup/font_unique_name_lookup_android.h index 649659a..cf8ad31 100644 --- a/content/browser/font_unique_name_lookup/font_unique_name_lookup_android.h +++ b/content/browser/font_unique_name_lookup/font_unique_name_lookup_android.h @@ -10,8 +10,10 @@ #include "base/files/file_path.h" #include "base/memory/read_only_shared_memory_region.h" +#include "base/sequence_checker.h" #include "base/synchronization/waitable_event.h" #include "base/task/sequenced_task_runner.h" +#include "base/thread_annotations.h" #include "content/common/content_export.h" #include "third_party/blink/public/mojom/font_unique_name_lookup/font_unique_name_lookup.mojom.h" @@ -114,8 +116,15 @@ base::FilePath TableCacheFilePath(); + // Dispatches all queued callbacks; runs exclusively on + // callback_access_runner_. void PostCallbacks(); + void EnqueueCallbackOnSequence( + scoped_refptr<base::SequencedTaskRunner> task_runner, + blink::mojom::FontUniqueNameLookup::GetUniqueNameLookupTableCallback + callback); + // We have a asynchronous update tasks which need write access to the // proto_storage_ MappedReadOnlyRegion after reading the index file from disk, // or after scanning and indexing metadata from font files. At the same time, @@ -126,6 +135,10 @@ base::WaitableEvent proto_storage_ready_; base::MappedReadOnlyRegion proto_storage_; + // Sequence runner dedicated to serializing pending_callbacks_ access: + scoped_refptr<base::SequencedTaskRunner> callback_access_runner_; + SEQUENCE_CHECKER(callback_sequence_checker_); + base::FilePath cache_directory_; std::string android_build_fingerprint_for_testing_; std::vector<base::FilePath> font_file_paths_for_testing_ = @@ -142,7 +155,8 @@ mojo_callback; }; - std::vector<CallbackOnTaskRunner> pending_callbacks_; + std::vector<CallbackOnTaskRunner> pending_callbacks_ + GUARDED_BY_CONTEXT(callback_sequence_checker_); }; } // namespace content diff --git a/content/browser/font_unique_name_lookup/font_unique_name_lookup_android_unittest.cc b/content/browser/font_unique_name_lookup/font_unique_name_lookup_android_unittest.cc index 4726b7f0..a3c2141 100644 --- a/content/browser/font_unique_name_lookup/font_unique_name_lookup_android_unittest.cc +++ b/content/browser/font_unique_name_lookup/font_unique_name_lookup_android_unittest.cc @@ -8,12 +8,18 @@ #include <memory> #include "base/android/android_info.h" +#include "base/barrier_closure.h" #include "base/files/file.h" #include "base/files/file_enumerator.h" #include "base/files/file_util.h" #include "base/files/scoped_temp_dir.h" #include "base/functional/function_ref.h" +#include "base/run_loop.h" #include "base/strings/string_util.h" +#include "base/task/sequenced_task_runner.h" +#include "base/task/thread_pool.h" +#include "base/test/bind.h" +#include "base/test/task_environment.h" #include "content/browser/font_unique_name_lookup/name_table_ffi.rs.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/common/font_unique_name_lookup/font_table_matcher.h" @@ -79,6 +85,7 @@ std::make_unique<FontUniqueNameLookup>(temp_dir_.GetPath()); } + base::test::TaskEnvironment task_environment_; base::ScopedTempDir temp_dir_; std::unique_ptr<FontUniqueNameLookup> font_unique_name_lookup_; }; @@ -311,6 +318,7 @@ font_file_corruptor_.GetFontFilesList()); } + base::test::TaskEnvironment task_environment_; base::ScopedTempDir temp_dir_; FontFileCorruptor font_file_corruptor_; std::unique_ptr<FontUniqueNameLookup> font_unique_name_lookup_; @@ -343,6 +351,7 @@ font_unique_name_lookup_->SetAndroidBuildFingerprintForTesting("A"); } + base::test::TaskEnvironment task_environment_; base::ScopedTempDir lookup_table_storage_dir; std::unique_ptr<FontUniqueNameLookup> font_unique_name_lookup_; }; @@ -364,4 +373,94 @@ ASSERT_TRUE(matcher_initial.FontListIsDisjointFrom(matcher_second_half)); } +class TestFontUniqueNameLookup : public FontUniqueNameLookup { + public: + using FontUniqueNameLookup::FontUniqueNameLookup; + using FontUniqueNameLookup::ScheduleLoadOrUpdateTable; +}; + +class FontUniqueNameLookupCallbackTest : public ::testing::Test { + protected: + void SetUp() override { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + lookup_ = std::make_unique<TestFontUniqueNameLookup>(temp_dir_.GetPath()); + std::vector<base::FilePath> font_files = AndroidFontFilesList(); + ASSERT_FALSE(font_files.empty()); + // Restrict to a single font to minimize test execution time and CPU load. + lookup_->SetFontFilePathsForTesting({font_files.front()}); + } + + base::test::TaskEnvironment task_environment_; + base::ScopedTempDir temp_dir_; + std::unique_ptr<TestFontUniqueNameLookup> lookup_; +}; + +TEST_F(FontUniqueNameLookupCallbackTest, ConcurrentCallbackQueuing) { + constexpr size_t kPreQueueCount = 10; + constexpr size_t kConcurrentThreads = 4; + constexpr size_t kPerThreadQueueCount = 5; + constexpr size_t kTotalCallbacks = + kPreQueueCount + (kConcurrentThreads * kPerThreadQueueCount); + + base::RunLoop run_loop; + base::RepeatingClosure barrier = + base::BarrierClosure(kTotalCallbacks, run_loop.QuitClosure()); + + auto make_callback = [&]() { + return base::BindLambdaForTesting( + [&, barrier](base::ReadOnlySharedMemoryRegion region) { + EXPECT_TRUE(region.IsValid()); + base::ReadOnlySharedMemoryMapping mapping = region.Map(); + EXPECT_TRUE(mapping.IsValid()); + blink::FontTableMatcher matcher(mapping); + EXPECT_GT(matcher.AvailableFonts(), 0u); + barrier.Run(); + }); + }; + + // Phase A: Pre-queue callbacks before table build starts. + // proto_storage_ready_ is guaranteed to be unsignaled, ensuring all callbacks + // are routed to callback_access_runner_ and enqueued in pending_callbacks_. + for (size_t i = 0; i < kPreQueueCount; ++i) { + scoped_refptr<base::SequencedTaskRunner> caller_runner = + base::ThreadPool::CreateSequencedTaskRunner({}); + lookup_->QueueShareMemoryRegionWhenReady(caller_runner, make_callback()); + } + + // Start asynchronous table build on ThreadPool. + lookup_->ScheduleLoadOrUpdateTable(); + + // Phase B: Concurrently queue callbacks from multiple threads while table + // build and drain are in-flight. + for (size_t i = 0; i < kConcurrentThreads; ++i) { + base::ThreadPool::PostTask( + FROM_HERE, base::BindLambdaForTesting([&]() { + for (size_t j = 0; j < kPerThreadQueueCount; ++j) { + scoped_refptr<base::SequencedTaskRunner> caller_runner = + base::ThreadPool::CreateSequencedTaskRunner({}); + lookup_->QueueShareMemoryRegionWhenReady(caller_runner, + make_callback()); + } + })); + } + + // Wait for all pre-queued and concurrently queued callbacks to complete. + run_loop.Run(); + + // Phase C: Late / fast-path queueing verification. + // Callbacks queued after the table is built should be dispatched directly. + base::RunLoop late_run_loop; + lookup_->QueueShareMemoryRegionWhenReady( + base::SequencedTaskRunner::GetCurrentDefault(), + base::BindLambdaForTesting([&](base::ReadOnlySharedMemoryRegion region) { + EXPECT_TRUE(region.IsValid()); + base::ReadOnlySharedMemoryMapping mapping = region.Map(); + EXPECT_TRUE(mapping.IsValid()); + blink::FontTableMatcher matcher(mapping); + EXPECT_GT(matcher.AvailableFonts(), 0u); + late_run_loop.Quit(); + })); + late_run_loop.Run(); +} + } // namespace content
Regression Test / PoC
diff --git a/content/browser/font_unique_name_lookup/font_unique_name_lookup_android_unittest.cc b/content/browser/font_unique_name_lookup/font_unique_name_lookup_android_unittest.cc
index 4726b7f0..a3c2141 100644
--- a/content/browser/font_unique_name_lookup/font_unique_name_lookup_android_unittest.cc
+++ b/content/browser/font_unique_name_lookup/font_unique_name_lookup_android_unittest.cc
@@ -8,12 +8,18 @@
#include <memory>
#include "base/android/android_info.h"
+#include "base/barrier_closure.h"
#include "base/files/file.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/function_ref.h"
+#include "base/run_loop.h"
#include "base/strings/string_util.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/task/thread_pool.h"
+#include "base/test/bind.h"
+#include "base/test/task_environment.h"
#include "content/browser/font_unique_name_lookup/name_table_ffi.rs.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/font_unique_name_lookup/font_table_matcher.h"
@@ -79,6 +85,7 @@
std::make_unique<FontUniqueNameLookup>(temp_dir_.GetPath());
}
+ base::test::TaskEnvironment task_environment_;
base::ScopedTempDir temp_dir_;
std::unique_ptr<FontUniqueNameLookup> font_unique_name_lookup_;
};
@@ -311,6 +318,7 @@
font_file_corruptor_.GetFontFilesList());
}
+ base::test::TaskEnvironment task_environment_;
base::ScopedTempDir temp_dir_;
FontFileCorruptor font_file_corruptor_;
std::unique_ptr<FontUniqueNameLookup> font_unique_name_lookup_;
@@ -343,6 +351,7 @@
font_unique_name_lookup_->SetAndroidBuildFingerprintForTesting("A");
}
+ base::test::TaskEnvironment task_environment_;
base::ScopedTempDir lookup_table_storage_dir;
std::unique_ptr<FontUniqueNameLookup> font_unique_name_lookup_;
};
@@ -364,4 +373,94 @@
ASSERT_TRUE(matcher_initial.FontListIsDisjointFrom(matcher_second_half));
}
+class TestFontUniqueNameLookup : public FontUniqueNameLookup {
+ public:
+ using FontUniqueNameLookup::FontUniqueNameLookup;
+ using FontUniqueNameLookup::ScheduleLoadOrUpdateTable;
+};
+
+class FontUniqueNameLookupCallbackTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+ lookup_ = std::make_unique<TestFontUniqueNameLookup>(temp_dir_.GetPath());
+ std::vector<base::FilePath> font_files = AndroidFontFilesList();
+ ASSERT_FALSE(font_files.empty());
+ // Restrict to a single font to minimize test execution time and CPU load.
+ lookup_->SetFontFilePathsForTesting({font_files.front()});
+ }
+
+ base::test::TaskEnvironment task_environment_;
+ base::ScopedTempDir temp_dir_;
+ std::unique_ptr<TestFontUniqueNameLookup> lookup_;
+};
+
+TEST_F(FontUniqueNameLookupCallbackTest, ConcurrentCallbackQueuing) {
+ constexpr size_t kPreQueueCount = 10;
+ constexpr size_t kConcurrentThreads = 4;
+ constexpr size_t kPerThreadQueueCount = 5;
+ constexpr size_t kTotalCallbacks =
+ kPreQueueCount + (kConcurrentThreads * kPerThreadQueueCount);
+
+ base::RunLoop run_loop;
+ base::RepeatingClosure barrier =
+ base::BarrierClosure(kTotalCallbacks, run_loop.QuitClosure());
+
+ auto make_callback = [&]() {
+ return base::BindLambdaForTesting(
+ [&, barrier](base::ReadOnlySharedMemoryRegion region) {
+ EXPECT_TRUE(region.IsValid());
+ base::ReadOnlySharedMemoryMapping mapping = region.Map();
+ EXPECT_TRUE(mapping.IsValid());
+ blink::FontTableMatcher matcher(mapping);
+ EXPECT_GT(matcher.AvailableFonts(), 0u);
+ barrier.Run();
+ });
+ };
+
+ // Phase A: Pre-queue callbacks before table build starts.
+ // proto_storage_ready_ is guaranteed to be unsignaled, ensuring all callbacks
+ // are routed to callback_access_runner_ and enqueued in pending_callbacks_.
+ for (size_t i = 0; i < kPreQueueCount; ++i) {
+ scoped_refptr<base::SequencedTaskRunner> caller_runner =
+ base::ThreadPool::CreateSequencedTaskRunner({});
+ lookup_->QueueShareMemoryRegionWhenReady(caller_runner, make_callback());
+ }
+
+ // Start asynchronous table build on ThreadPool.
+ lookup_->ScheduleLoadOrUpdateTable();
+
+ // Phase B: Concurrently queue callbacks from multiple threads while table
+ // build and drain are in-flight.
+ for (size_t i = 0; i < kConcurrentThreads; ++i) {
+ base::ThreadPool::PostTask(
+ FROM_HERE, base::BindLambdaForTesting([&]() {
+ for (size_t j = 0; j < kPerThreadQueueCount; ++j) {
+ scoped_refptr<base::SequencedTaskRunner> caller_runner =
+ base::ThreadPool::CreateSequencedTaskRunner({});
+ lookup_->QueueShareMemoryRegionWhenReady(caller_runner,
+ make_callback());
+ }
+ }));
+ }
+
+ // Wait for all pre-queued and concurrently queued callbacks to complete.
+ run_loop.Run();
+
+ // Phase C: Late / fast-path queueing verification.
+ // Callbacks queued after the table is built should be dispatched directly.
+ base::RunLoop late_run_loop;
+ lookup_->QueueShareMemoryRegionWhenReady(
+ base::SequencedTaskRunner::GetCurrentDefault(),
+ base::BindLambdaForTesting([&](base::ReadOnlySharedMemoryRegion region) {
+ EXPECT_TRUE(region.IsValid());
+ base::ReadOnlySharedMemoryMapping mapping = region.Map();
+ EXPECT_TRUE(mapping.IsValid());
+ blink::FontTableMatcher matcher(mapping);
+ EXPECT_GT(matcher.AvailableFonts(), 0u);
+ late_run_loop.Quit();
+ }));
+ late_run_loop.Run();
+}
+
} // namespace content
Original Bug Report
Cross-thread race on `FontUniqueNameLookup::pending_callbacks_` leads to browser process use-after-free
Cross-thread race on FontUniqueNameLookup::pending_callbacks_ leads to browser process use-after-free
Summary
The browser-process singleton content::FontUniqueNameLookup on Android accesses its pending_callbacks_ vector from two different threads without synchronization. A Mojo service sequence calls QueueShareMemoryRegionWhenReady() which performs emplace_back(), while a ThreadPool worker calls PostCallbacks() which iterates and clears the same vector. When emplace_back() triggers a vector reallocation during an ongoing PostCallbacks() iteration, the old backing buffer is freed while the iterator still references it, producing a use-after-free in the browser process. This affects Android only.
Bisect
Introducing Commit: a2dece8510578768eda1202047001188639802ba
- Date: 2019-06-13
- Author: Dominik Rottsches
- Review: https://chromium-review.googlesource.com/c/chromium/src/+/1655315
Root Cause
FontUniqueNameLookup is a NoDestructor singleton instantiated early during browser startup via PostCreateThreadsImpl. Its constructor calls ScheduleLoadOrUpdateTable(), which posts a BEST_EFFORT task to a ThreadPool worker to scan system fonts, build a lookup table, and then dispatch pending callbacks:
// content/browser/font_unique_name_lookup/font_unique_name_lookup_android.cc:212-235
void FontUniqueNameLookup::ScheduleLoadOrUpdateTable() {
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(
[](FontUniqueNameLookup* instance) {
instance->LoadFromFile();
if (instance->UpdateTableIfNeeded()) {
instance->PersistToFile();
}
instance->proto_storage_ready_.Signal();
instance->PostCallbacks();
},
base::Unretained(this)));
}
PostCallbacks() runs on that ThreadPool worker thread, iterating pending_callbacks_ via range-for and clearing it afterward:
// content/browser/font_unique_name_lookup/font_unique_name_lookup_android.cc:285-292
void FontUniqueNameLookup::PostCallbacks() {
for (auto& pending_callback : pending_callbacks_) {
pending_callback.task_runner->PostTask(
FROM_HERE, base::BindOnce(std::move(pending_callback.mojo_callback),
DuplicateMemoryRegion()));
}
pending_callbacks_.clear();
}
Concurrently, renderer processes request the font lookup table through a Mojo interface. The service handler runs on its own SequencedTaskRunner, separate from both the UI thread and the ThreadPool worker above. When the table is not yet ready, it queues a callback:
// content/browser/font_unique_name_lookup/font_unique_name_lookup_service.cc:44-53
void FontUniqueNameLookupService::GetUniqueNameLookupTable(
GetUniqueNameLookupTableCallback callback) {
DCHECK(GetTaskRunner()->RunsTasksInCurrentSequence());
if (font_unique_name_lookup_->IsValid()) {
std::move(callback).Run(font_unique_name_lookup_->DuplicateMemoryRegion());
} else {
font_unique_name_lookup_->QueueShareMemoryRegionWhenReady(
GetTaskRunner(), std::move(callback));
}
}
QueueShareMemoryRegionWhenReady() simply appends to the vector:
// content/browser/font_unique_name_lookup/font_unique_name_lookup_android.cc:145-149
void FontUniqueNameLookup::QueueShareMemoryRegionWhenReady(
scoped_refptr<base::SequencedTaskRunner> task_runner,
blink::mojom::FontUniqueNameLookup::GetUniqueNameLookupTableCallback
callback) {
pending_callbacks_.emplace_back(std::move(task_runner), std::move(callback));
}
The class declaration reveals no synchronization protecting pending_callbacks_:
// content/browser/font_unique_name_lookup/font_unique_name_lookup_android.h:134-145
struct CallbackOnTaskRunner {
CallbackOnTaskRunner(
scoped_refptr<base::SequencedTaskRunner>,
blink::mojom::FontUniqueNameLookup::GetUniqueNameLookupTableCallback);
CallbackOnTaskRunner(CallbackOnTaskRunner&&);
~CallbackOnTaskRunner();
scoped_refptr<base::SequencedTaskRunner> task_runner;
blink::mojom::FontUniqueNameLookup::GetUniqueNameLookupTableCallback
mojo_callback;
};
std::vector<CallbackOnTaskRunner> pending_callbacks_;
There is no mutex, no SEQUENCE_CHECKER, and no GUARDED_BY annotation. The WaitableEvent proto_storage_ready_ only synchronizes access to proto_storage_, not to pending_callbacks_.
The race unfolds as a TOCTOU: a service sequence call checks IsValid() and observes false (the table is not ready). Before it executes emplace_back(), the ThreadPool worker finishes building the table, calls Signal(), and enters PostCallbacks(). The service call then executes emplace_back() while PostCallbacks() is iterating. If the emplace_back() exceeds the vector’s capacity, std::vector reallocates its backing buffer and frees the old one. The range-for iterator in PostCallbacks() still points into the freed buffer; the subsequent dereference of pending_callback.task_runner is a use-after-free.
This bug is not mitigated by MiraclePtr. The dangling access occurs through a std::vector range-for iterator/reference, not through a raw_ptr<T> field. MiraclePtr only protects raw_ptr<T> smart pointer fields allocated through PartitionAlloc. STL container iterators and element references are entirely outside its scope.
From a renderer, the attack surface is the FontFace API. new FontFace('name', 'local(...)').load() triggers LocalFontFaceSource::BeginLoadIfNeeded(), which calls through to FontUniqueNameLookupAndroid::PrepareFontUniqueNameLookup(), sending a GetUniqueNameLookupTable Mojo IPC to the browser. With kPrefetchFontLookupTables enabled by default on Android, each new renderer process also sends this IPC automatically during initialization.
Reproduce
Tested at commit 457566e1c0b4197b30d7532c4d7430b2ef738765 on Android (Pixel 7 Pro, HWASAN userdebug build).
The attached patch.diff inserts Sleep() calls to make the race deterministically reproducible under HWASAN. The patch makes four changes:
- In the renderer, it removes the IPC deduplication guard in
PrepareFontUniqueNameLookup()so that eachFontFace.load()call sends a separateGetUniqueNameLookupTableIPC to the browser, simulating a compromised renderer flooding the service. - In the browser service handler (
GetUniqueNameLookupTable), it inserts a 2-second sleep between theIsValid()check and theemplace_back()call, widening the TOCTOU window so thatSignal()andPostCallbacks()can begin on the ThreadPool worker while the service sequence is still about to append. - In
PostCallbacks(), it inserts a 5-second sleep per element iteration, keeping the range-for loop alive long enough for a concurrentemplace_back()to land and trigger vector reallocation mid-iteration. - In
ScheduleLoadOrUpdateTable(), it inserts a 6-second initial delay before the font table build begins, ensuring renderer IPCs arrive and queue callbacks before the table becomes valid.
Build configuration (out/android/args.gn):
target_os = "android"
target_cpu = "arm64"
is_debug = false
dcheck_always_on = false
is_component_build = false
is_hwasan = true
android_static_analysis = "off"
Apply the patch and build:
cd ~/chromium/src
git apply patch.diff
autoninja -C out/android chrome_public_apk
out/android/bin/chrome_public_apk install
The --no-first-run --disable-fre flags skip the Android First Run Experience dialog, which would otherwise block page loading and prevent the PoC from executing during the startup window:
adb shell am force-stop org.chromium.chrome
adb logcat -c
python3 -m http.server 8880 --bind 0.0.0.0 &
adb reverse tcp:8880 tcp:8880
out/android/bin/chrome_public_apk launch \
--args='--no-first-run --disable-fre --enable-logging=stderr' \
http://localhost:8880/poc.html
The browser process crashes with an HWASAN tag-mismatch within approximately 16 seconds.
Symbolized stack:
#0 scoped_refptr<base::SequencedTaskRunner>::operator->() const
base/memory/scoped_refptr.h:293
content::FontUniqueNameLookup::PostCallbacks()
content/browser/font_unique_name_lookup/font_unique_name_lookup_android.cc:292
#1 base::OnceCallback<void ()>::Run()
base/functional/callback.h:155
base::TaskAnnotator::RunTaskImpl()
base/task/common/task_annotator.cc:229
#2 base::internal::TaskTracker::RunTaskImpl()
base/task/thread_pool/task_tracker.cc:691
...
#9 base::(anonymous namespace)::ThreadFunc(void*)
base/threading/platform_thread_posix.cc:102
Full HWASAN report:
==5416==ERROR: HWAddressSanitizer: tag-mismatch on address 0x0043d831ade0 at pc 0x00666bc49608
READ of size 8 at 0x0043d831ade0 tags: 9f/26 (ptr/mem) in thread T113
#0 0x00666bc49608 (libchrome.so+0x7d6f608)
#1 0x00666ffb2770 (libchrome.so+0xc0d8770)
#2 0x0066700116cc (libchrome.so+0xc1376cc)
#3 0x006670011504 (libchrome.so+0xc137504)
#4 0x0066700109cc (libchrome.so+0xc1369cc)
#5 0x006670010018 (libchrome.so+0xc136018)
#6 0x006670022148 (libchrome.so+0xc148148)
#7 0x00667002181c (libchrome.so+0xc14781c)
#8 0x0066700214d8 (libchrome.so+0xc1474d8)
#9 0x0066700636e0 (libchrome.so+0xc1896e0)
#10 0x0070c0cfdf2c (libc.so+0x96f2c)
#11 0x0070c0ce73d0 (libc.so+0x803d0)
[0x0043d831ade0,0x0043d831ae00) is a small allocated heap chunk; size: 32 offset: 0
Memory tags around the buggy address (one tag corresponds to 16 bytes):
0x0043d831ac00: 0b fc 04 00 ed 08 14 00 47 47 08 00 62 06 5a 5a
=>0x0043d831ad00: ea ea 08 00 64 01 0b d8 7b 06 df cf f6 08 [26] 26
0x0043d831ae00: 80 00 c3 c3 5d 5d 93 00 65 00 8b 8b 3b 3b 1d 00
SUMMARY: HWAddressSanitizer: tag-mismatch (libchrome.so+0x7d6f608)
Credit
Please use c6eed09fc8b174b0f3eebedcceb1e792 as the credit for this vulnerability. Thank you.
References
- font_unique_name_lookup_android.cc (PostCallbacks)
- font_unique_name_lookup_android.cc (QueueShareMemoryRegionWhenReady)
- font_unique_name_lookup_android.cc (ScheduleLoadOrUpdateTable)
- font_unique_name_lookup_service.cc (GetUniqueNameLookupTable)
- font_unique_name_lookup_android.h (pending_callbacks_ declaration)
- http://localhost:8880/poc.html
- https://chromium-review.googlesource.com/c/chromium/src/+/1655315
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/font_unique_name_lookup/font_unique_name_lookup_android.cc;l=145-149
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/font_unique_name_lookup/font_unique_name_lookup_android.cc;l=212-235
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/font_unique_name_lookup/font_unique_name_lookup_android.cc;l=285-292
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/font_unique_name_lookup/font_unique_name_lookup_android.h;l=134-145
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/font_unique_name_lookup/font_unique_name_lookup_service.cc;l=44-53