CVE-2026-11071
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifbase/nix/mime_util_xdg.cc |
modified | |
MimeWorkerThreadbase/nix/mime_util_xdg_unittest.cc |
modified | |
TESTbase/nix/mime_util_xdg_unittest.cc |
modified | |
forbase/nix/mime_util_xdg_unittest.cc |
modified | |
ifbase/nix/mime_util_xdg_unittest.cc |
modified |
Files Changed
base/nix/mime_util_xdg.ccbase/nix/mime_util_xdg_unittest.cc
Patch
From c512ccdd9447b3fa0e5c0170450196dbf14a8e05 Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner <[email protected]> Date: Tue, 07 Apr 2026 14:34:24 -0700 Subject: [PATCH] Fix data race in base::nix::GetFileMimeType Extend the AutoLock scope in GetFileMimeType to cover the map lookup and result extraction. This prevents a potential Use-After-Free if one thread clears and reloads the map while another thread is accessing an iterator outside the lock. Fixed: 499227659 Change-Id: I0636bd2224a9b7719b8525cd4f79c50fa5a1a4f6 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7736162 Reviewed-by: Daniel Cheng <[email protected]> Commit-Queue: Andrew Paseltiner <[email protected]> Cr-Commit-Position: refs/heads/main@{#1610995} --- diff --git a/base/nix/mime_util_xdg.cc b/base/nix/mime_util_xdg.cc index 36396fd..fccc597 100644 --- a/base/nix/mime_util_xdg.cc +++ b/base/nix/mime_util_xdg.cc @@ -257,24 +257,24 @@ // check every 5s and reload if any files have changed. #if !BUILDFLAG(IS_CHROMEOS) static Time last_check; - // Lock is required since this may be called on any thread. + // Lock is required since this may be called on any thread. The lock is held + // until the function returns to ensure that the map lookup and result copy + // are thread-safe if a reload occurs concurrently. static NoDestructor<Lock> lock; - { - AutoLock scoped_lock(*lock); + AutoLock scoped_lock(*lock); - Time now = Time::Now(); - if (last_check + Seconds(5) < now) { - if (std::ranges::any_of(*xdg_mime_files, [](const FileInfo& file_info) { - File::Info info; - return !GetFileInfo(file_info.path, &info) || - info.last_modified != file_info.last_modified; - })) { - mime_type_map->clear(); - xdg_mime_files->clear(); - LoadAllMimeCacheFiles(*mime_type_map, *xdg_mime_files); - } - last_check = now; + Time now = Time::Now(); + if (last_check + Seconds(5) < now) { + if (std::ranges::any_of(*xdg_mime_files, [](const FileInfo& file_info) { + File::Info info; + return !GetFileInfo(file_info.path, &info) || + info.last_modified != file_info.last_modified; + })) { + mime_type_map->clear(); + xdg_mime_files->clear(); + LoadAllMimeCacheFiles(*mime_type_map, *xdg_mime_files); } + last_check = now; } #endif diff --git a/base/nix/mime_util_xdg_unittest.cc b/base/nix/mime_util_xdg_unittest.cc index 4e002b7..d8c9600 100644 --- a/base/nix/mime_util_xdg_unittest.cc +++ b/base/nix/mime_util_xdg_unittest.cc @@ -4,6 +4,7 @@ #include "base/nix/mime_util_xdg.h" +#include <atomic> #include <map> #include <string> #include <vector> @@ -13,6 +14,12 @@ #include "base/files/file_path.h" #include "base/files/file_util.h" #include "base/files/scoped_temp_dir.h" +#include "base/memory/raw_ptr.h" +#include "base/scoped_environment_variable_override.h" +#include "base/synchronization/waitable_event.h" +#include "base/test/task_environment.h" +#include "base/threading/platform_thread.h" +#include "base/threading/simple_thread.h" #include "testing/gtest/include/gtest/gtest.h" namespace base::nix { @@ -79,6 +86,22 @@ FilePath mime_types_path_; }; +class MimeWorkerThread : public base::SimpleThread { + public: + MimeWorkerThread(base::WaitableEvent* event, std::atomic<bool>* stop) + : SimpleThread("MimeWorkerThread"), event_(event), stop_(stop) {} + void Run() override { + event_->Wait(); + while (!stop_->load()) { + GetFileMimeType(base::FilePath("foo.pdf")); + } + } + + private: + const raw_ptr<base::WaitableEvent> event_; + const raw_ptr<std::atomic<bool>> stop_; +}; + } // namespace bool operator==(const WeightedMime& lhs, const WeightedMime& rhs) { @@ -175,4 +198,59 @@ InvalidIf(*buf, 0x18b, 0x74); } +// Regression test for crbug.com/499227659. Ensure that concurrent calls to +// GetFileMimeType and reloading the MIME cache doesn't crash or cause a data +// race. +TEST(MimeUtilXdgTest, GetFileMimeTypeRace) { + base::test::TaskEnvironment task_environment( + base::test::TaskEnvironment::TimeSource::MOCK_TIME); + + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + base::FilePath mime_dir = temp_dir.GetPath().Append("mime"); + ASSERT_TRUE(CreateDirectory(mime_dir)); + base::FilePath mime_cache = mime_dir.Append("mime.cache"); + + auto buf = Base64Decode(kTestMimeCacheB64); + ASSERT_TRUE(buf.has_value()); + ASSERT_TRUE(WriteFile(mime_cache, *buf)); + + base::ScopedEnvironmentVariableOverride env_override( + "XDG_DATA_HOME", temp_dir.GetPath().value()); + + // Call once to initialize static variables. + GetFileMimeType(base::FilePath("foo.pdf")); + + std::atomic<bool> stop{false}; + base::WaitableEvent event(base::WaitableEvent::ResetPolicy::MANUAL, + base::WaitableEvent::InitialState::NOT_SIGNALED); + std::vector<std::unique_ptr<MimeWorkerThread>> threads; + + for (int i = 0; i < 20; ++i) { + threads.emplace_back(std::make_unique<MimeWorkerThread>(&event, &stop)) + ->Start(); + } + + event.Signal(); + + for (int i = 0; i < 1000; ++i) { + // Update file time. + base::File::Info info; + GetFileInfo(mime_cache, &info); + base::TouchFile(mime_cache, info.last_accessed, + info.last_modified + base::Seconds(1)); + + // Advance time to bypass 5s check. + task_environment.FastForwardBy(base::Seconds(6)); + if (i % 100 == 0) { + base::PlatformThread::Sleep(base::Milliseconds(1)); + } + } + + stop = true; + for (auto& t : threads) { + t->Join(); + } +} + } // namespace base::nix
Regression Test / PoC
diff --git a/base/nix/mime_util_xdg_unittest.cc b/base/nix/mime_util_xdg_unittest.cc
index 4e002b7..d8c9600 100644
--- a/base/nix/mime_util_xdg_unittest.cc
+++ b/base/nix/mime_util_xdg_unittest.cc
@@ -4,6 +4,7 @@
#include "base/nix/mime_util_xdg.h"
+#include <atomic>
#include <map>
#include <string>
#include <vector>
@@ -13,6 +14,12 @@
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
+#include "base/memory/raw_ptr.h"
+#include "base/scoped_environment_variable_override.h"
+#include "base/synchronization/waitable_event.h"
+#include "base/test/task_environment.h"
+#include "base/threading/platform_thread.h"
+#include "base/threading/simple_thread.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace base::nix {
@@ -79,6 +86,22 @@
FilePath mime_types_path_;
};
+class MimeWorkerThread : public base::SimpleThread {
+ public:
+ MimeWorkerThread(base::WaitableEvent* event, std::atomic<bool>* stop)
+ : SimpleThread("MimeWorkerThread"), event_(event), stop_(stop) {}
+ void Run() override {
+ event_->Wait();
+ while (!stop_->load()) {
+ GetFileMimeType(base::FilePath("foo.pdf"));
+ }
+ }
+
+ private:
+ const raw_ptr<base::WaitableEvent> event_;
+ const raw_ptr<std::atomic<bool>> stop_;
+};
+
} // namespace
bool operator==(const WeightedMime& lhs, const WeightedMime& rhs) {
@@ -175,4 +198,59 @@
InvalidIf(*buf, 0x18b, 0x74);
}
+// Regression test for crbug.com/499227659. Ensure that concurrent calls to
+// GetFileMimeType and reloading the MIME cache doesn't crash or cause a data
+// race.
+TEST(MimeUtilXdgTest, GetFileMimeTypeRace) {
+ base::test::TaskEnvironment task_environment(
+ base::test::TaskEnvironment::TimeSource::MOCK_TIME);
+
+ base::ScopedTempDir temp_dir;
+ ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+ base::FilePath mime_dir = temp_dir.GetPath().Append("mime");
+ ASSERT_TRUE(CreateDirectory(mime_dir));
+ base::FilePath mime_cache = mime_dir.Append("mime.cache");
+
+ auto buf = Base64Decode(kTestMimeCacheB64);
+ ASSERT_TRUE(buf.has_value());
+ ASSERT_TRUE(WriteFile(mime_cache, *buf));
+
+ base::ScopedEnvironmentVariableOverride env_override(
+ "XDG_DATA_HOME", temp_dir.GetPath().value());
+
+ // Call once to initialize static variables.
+ GetFileMimeType(base::FilePath("foo.pdf"));
+
+ std::atomic<bool> stop{false};
+ base::WaitableEvent event(base::WaitableEvent::ResetPolicy::MANUAL,
+ base::WaitableEvent::InitialState::NOT_SIGNALED);
+ std::vector<std::unique_ptr<MimeWorkerThread>> threads;
+
+ for (int i = 0; i < 20; ++i) {
+ threads.emplace_back(std::make_unique<MimeWorkerThread>(&event, &stop))
+ ->Start();
+ }
+
+ event.Signal();
+
+ for (int i = 0; i < 1000; ++i) {
+ // Update file time.
+ base::File::Info info;
+ GetFileInfo(mime_cache, &info);
+ base::TouchFile(mime_cache, info.last_accessed,
+ info.last_modified + base::Seconds(1));
+
+ // Advance time to bypass 5s check.
+ task_environment.FastForwardBy(base::Seconds(6));
+ if (i % 100 == 0) {
+ base::PlatformThread::Sleep(base::Milliseconds(1));
+ }
+ }
+
+ stop = true;
+ for (auto& t : threads) {
+ t->Join();
+ }
+}
+
} // namespace base::nix
Original Bug Report
UAF read in base::nix::GetFileMimeType via Data Race
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 security team.
Overview: A data race in base::nix::GetFileMimeType on Linux can lead to a Use-After-Free (UAF) read of a std::map node. Concurrent Mojo requests from compromised renderers can trigger a map reload while an iterator is actively being accessed outside the lock. An attacker could potentially exploit this to leak arbitrary browser process memory and bypass ASLR.
Affected files:
base/nix/mime_util_xdg.cccontent/browser/mime_registry_impl.ccnet/base/platform_mime_util_linux.cc
Estimated timestamp from git blame: 2023-11-02
Summary
There is a potential data race and Use-After-Free (UAF) vulnerability in the Linux implementation of base::nix::GetFileMimeType. The function caches MIME types in a static std::map, protected by a lock during periodic reloads. However, the lock is released immediately before searching the map and dereferencing the returned iterator. If one thread accesses the iterator while another thread concurrently clears the map, a UAF occurs.
Vulnerability Details
In base/nix/mime_util_xdg.cc, the GetFileMimeType function uses an AutoLock to check if ~/.local/share/mime/ cache files have changed (checked at most every 5 seconds). If they have, it calls mime_type_map->clear() to reload the cache.
{
AutoLock scoped_lock(*lock);
// ... 5-second check and modification time check ...
if (files_changed) {
mime_type_map->clear();
// ... reload ...
}
} // Lock released
auto it = mime_type_map->find(ext.substr(1));
return it != mime_type_map->end() ? it->second.mime_type : std::string();
The lock scope ends before mime_type_map->find() and before the returned iterator it is dereferenced.
Because content::MimeRegistryImpl::Create provisions a new SequencedTaskRunner per RenderProcessHost, Mojo calls to MimeRegistry::GetMimeTypeFromExtension from different renderer processes will execute concurrently in the browser process’s ThreadPool.
Potential Exploitation Steps
An attacker with code execution in a sandboxed renderer could potentially exploit this to read arbitrary browser process memory (an ASLR bypass) through the following steps:
- The attacker compromises two separate renderer processes (e.g., via iframes forcing Site Isolation to different origins) to gain concurrent execution threads in the browser process.
- The attacker triggers a modification to the XDG mime cache files. This can be done without user interaction by performing a silent manifest update on a previously installed Progressive Web App (PWA) to add new file handlers, which invokes
xdg-mime install. - The attacker spams synchronous
MimeRegistry::GetMimeTypeFromExtensionMojo messages from both renderers. - Thread A enters
GetFileMimeType, observes the cache is up-to-date, exits the lock, and callsmime_type_map->find(), obtaining a valid iterator. Thread A is then preempted. - Thread B enters
GetFileMimeType, observes the 5-second boundary and file modifications, and callsmime_type_map->clear(). This immediately destructs and frees allstd::_Rb_tree_nodes in the map back to the PartitionAlloc Main Partition. - The attacker rapidly sprays the browser process heap via Mojo string allocations. By sending exactly 88-byte strings, they can reclaim the freed
std::_Rb_tree_node(which houses a 24-bytestd::stringand a 32-byteWeightedMimepayload on 64-bit libc++). - The payload is crafted to forge the internal pointer and size fields of the
std::stringinside theWeightedMimestruct, pointing it to an arbitrary target address in the browser heap. - Thread A resumes and executes
return it->second.mime_type. Thestd::stringcopy constructor is invoked on the forged string. - Note on MiraclePtr: MiraclePtr (BRP) does not prevent this because the internal pointers of libc++ standard library containers (
std::mapnodes andstd::stringbuffers) use rawT*pointers, notbase::raw_ptr<T>. - The copy constructor reads the targeted memory, and the resulting string is returned to the renderer via the Mojo callback, successfully leaking the data.
(Note: These are suggested steps for exploitation; a working proof-of-concept has not been fully verified by our tooling yet.)
Suggested Fix
The lock scope must be extended to cover both the map lookup and the extraction of the value. A local std::string can be used to hold the result before returning:
std::string result;
{
AutoLock scoped_lock(*lock);
Time now = Time::Now();
// ... cache invalidation logic ...
auto it = mime_type_map->find(ext.substr(1));
if (it != mime_type_map->end()) {
result = it->second.mime_type;
}
}
return result;
This ensures the iterator and the underlying node cannot be invalidated by another thread before the string is safely copied.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
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.