Critical watch CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in XR
DescriptionUse after free in XR
ComponentXR
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker513135985
Fix commit62dc8431a8fd (chromium/src) +64/-13
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-14 (per vendor advisory)
Fix landed2026-05-15 (public git)
Disclosed2026-05-27
Patch gap12 days — fix public in source before the advisory

Background

`switches::kUseAdapterLuid`
a Windows-only GPU-process switch giving the LUID of the graphics adapter the GPU process should use. XR uses it so the GL context is created on the GPU the headset is plugged into.
`base::CommandLine::ForCurrentProcess()`
the browser process’s singleton command line. It is meant to be set up at startup and then treated as read-only, because many threads read it without synchronization.
`GpuDataManagerImpl` / `GpuDataManagerImplPrivate`
the browser-side GPU state manager. Its public methods take lock_ and delegate to the private object, which, among other things, builds the GPU process command line (AppendGpuCommandLine).
`XRRuntimeManagerImpl`
the browser-side WebXR runtime manager. When a runtime needs a specific adapter, it restarts the GPU process on that adapter (xr_compatible_restarted_gpu_) and later restores the default.

Root Cause Analysis

Before the patch, XRRuntimeManagerImpl passed the XR adapter to the GPU process by mutating the browser’s global command line at runtime. It called base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(switches::kUseAdapterLuid, ...) when a runtime reported a LUID, and RemoveSwitch(switches::kUseAdapterLuid) in its teardown path. GpuProcessHost then copied the switch into the GPU child’s command line through its switch-forwarding list. base::CommandLine is not thread-safe for mutation: appending or removing a switch rewrites its internal argv/switch storage. Any other thread reading the process command line at the same time, or code holding a reference into that storage, can therefore touch freed memory. That is consistent with the advisory’s use-after-free (the specific concurrent reader is not shown in the diff; inferred).

The fix stops mutating the process command line. The LUID is kept in GpuDataManagerImplPrivate::use_adapter_luid_, accessed only through lock_-guarded SetUseAdapterLuid / ClearUseAdapterLuid / GetUseAdapterLuid, and emitted directly into the GPU child’s command line when it is built. An explicit browser --use-adapter-luid still takes precedence. kUseAdapterLuid is removed from GpuProcessHost’s copy list so the value comes only from the new path.

Key insight
Feature code treated the process-global CommandLine as a mutable, cross-thread communication channel at runtime. The fix moves that state into a lock-protected field of GpuDataManagerImplPrivate and injects it only into the child process’s own command line.

Attack Path

  1. Enter WebXR on Windows A page requests an immersive WebXR session, or calls makeXRCompatible, on a Windows system whose XR runtime reports a specific adapter LUID. This reaches the code path in xr_runtime_manager_impl.cc that DCHECKs a non-zero luid.
  2. Global command line mutated Before the patch, the browser UI thread appended --use-adapter-luid to CommandLine::ForCurrentProcess() and restarted the GPU process.
  3. Repeat or tear down Ending XR use so that the XRRuntimeManagerImpl is destroyed (for example, by navigating away) ran RemoveSwitch on the same global object; a page can cycle these transitions (inferred).
  4. Concurrent reader hits freed storage Another browser thread reading the process command line during an append or remove observes reallocated or freed storage, which is the use-after-free the advisory reports (the specific reader is inferred).

Impact Assessment

The vendor rates this a critical use-after-free in XR. Because the mutated object is the browser process’s own command line, the memory corruption would occur in the browser process (inferred from the code location), which is consistent with that rating. Preconditions are Windows, an XR runtime that specifies an adapter LUID, and a page that uses WebXR. The race is timing-dependent, and the advisory does not report in-the-wild exploitation.

Changed Functions

FunctionChangeNotes
if
content/browser/xr/service/xr_runtime_manager_impl.cc
modified

Files Changed

  • content/browser/gpu/gpu_data_manager_impl.cc
  • content/browser/gpu/gpu_data_manager_impl.h
  • content/browser/gpu/gpu_data_manager_impl_private.cc
  • content/browser/gpu/gpu_data_manager_impl_private.h
  • content/browser/gpu/gpu_process_host.cc
  • content/browser/xr/service/xr_runtime_manager_impl.cc

Audit Directions

  • Runtime mutation of `CommandLine::ForCurrentProcess()`
    Grep for AppendSwitch* / RemoveSwitch on the process command line outside early startup; every such call racing with readers on other threads is a candidate UAF.
  • Process-global state as an IPC side channel
    Look for features that pass per-session configuration to child processes by editing globals (environment variables, the command line, static singletons) instead of per-launch parameters.
  • Feature teardown in destructors
    Review destructors such as XRRuntimeManagerImpl’s that undo global changes, checking for ordering against concurrent child-process launches and for an undo that runs even if the change was made by a different instance.
diff --git a/content/browser/gpu/gpu_data_manager_impl.cc b/content/browser/gpu/gpu_data_manager_impl.cc
index 1043afb0..a3ad2e7 100644
--- a/content/browser/gpu/gpu_data_manager_impl.cc
+++ b/content/browser/gpu/gpu_data_manager_impl.cc
@@ -231,6 +231,21 @@
   base::AutoLock auto_lock(lock_);
   private_->TerminateInfoCollectionGpuProcess();
 }
+
+void GpuDataManagerImpl::SetUseAdapterLuid(const CHROME_LUID& luid) {
+  base::AutoLock auto_lock(lock_);
+  private_->SetUseAdapterLuid(luid);
+}
+
+void GpuDataManagerImpl::ClearUseAdapterLuid() {
+  base::AutoLock auto_lock(lock_);
+  private_->ClearUseAdapterLuid();
+}
+
+std::optional<CHROME_LUID> GpuDataManagerImpl::GetUseAdapterLuid() const {
+  base::AutoLock auto_lock(lock_);
+  return private_->GetUseAdapterLuid();
+}
 #endif  // BUILDFLAG(IS_WIN)
 
 void GpuDataManagerImpl::PostCreateThreads() {
diff --git a/content/browser/gpu/gpu_data_manager_impl.h b/content/browser/gpu/gpu_data_manager_impl.h
index a360401..a2e44d7 100644
--- a/content/browser/gpu/gpu_data_manager_impl.h
+++ b/content/browser/gpu/gpu_data_manager_impl.h
@@ -9,6 +9,7 @@
 #include <stdint.h>
 
 #include <memory>
+#include <optional>
 #include <string>
 
 #include "base/no_destructor.h"
@@ -34,6 +35,7 @@
 #include "ui/gfx/gpu_extra_info.h"
 
 #if BUILDFLAG(IS_WIN)
+#include "base/win/windows_types.h"
 #include "ui/gfx/mojom/dxgi_info.mojom.h"
 #endif
 
@@ -122,6 +124,13 @@
   bool DirectXRequested() const;
   bool VulkanRequested() const;
   void TerminateInfoCollectionGpuProcess();
+
+  // Information to Get/Set the LUID that the GPU Process should be launched on.
+  // Predominantly used by XR, so that we can ensure the GL context is created
+  // on the GPU that the headset is actually plugged into.
+  void SetUseAdapterLuid(const CHROME_LUID& luid);
+  void ClearUseAdapterLuid();
+  std::optional<CHROME_LUID> GetUseAdapterLuid() const;
 #endif
   // Called from BrowserMainLoop::PostCreateThreads().
   // TODO(content/browser/gpu/OWNERS): This should probably use a
diff --git a/content/browser/gpu/gpu_data_manager_impl_private.cc b/content/browser/gpu/gpu_data_manager_impl_private.cc
index 5518072b..97e9f0ac 100644
--- a/content/browser/gpu/gpu_data_manager_impl_private.cc
+++ b/content/browser/gpu/gpu_data_manager_impl_private.cc
@@ -33,6 +33,7 @@
 #include "base/metrics/histogram_macros.h"
 #include "base/path_service.h"
 #include "base/rand_util.h"
+#include "base/strings/string_number_conversions.h"
 #include "base/strings/string_util.h"
 #include "base/task/bind_post_task.h"
 #include "base/trace_event/trace_event.h"
@@ -1156,6 +1157,19 @@
   if (host)
     host->ForceShutdown();
 }
+
+void GpuDataManagerImplPrivate::SetUseAdapterLuid(const CHROME_LUID& luid) {
+  use_adapter_luid_ = luid;
+}
+
+void GpuDataManagerImplPrivate::ClearUseAdapterLuid() {
+  use_adapter_luid_ = std::nullopt;
+}
+
+std::optional<CHROME_LUID> GpuDataManagerImplPrivate::GetUseAdapterLuid()
+    const {
+  return use_adapter_luid_;
+}
 #endif
 
 void GpuDataManagerImplPrivate::PostCreateThreads() {
@@ -1358,6 +1372,19 @@
   if (!use_gl.empty()) {
     command_line->AppendSwitchASCII(switches::kUseGL, use_gl);
   }
+
+#if BUILDFLAG(IS_WIN)
+  if (browser_command_line->HasSwitch(switches::kUseAdapterLuid)) {
+    command_line->AppendSwitchASCII(
+        switches::kUseAdapterLuid,
+        browser_command_line->GetSwitchValueASCII(switches::kUseAdapterLuid));
+  } else if (use_adapter_luid_.has_value()) {
+    std::string luid_string =
+        base::NumberToString(use_adapter_luid_->HighPart) + "," +
+        base::NumberToString(use_adapter_luid_->LowPart);
+    command_line->AppendSwitchASCII(switches::kUseAdapterLuid, luid_string);
+  }
+#endif
 }
 
 void GpuDataManagerImplPrivate::UpdateGpuPreferences(
diff --git a/content/browser/gpu/gpu_data_manager_impl_private.h b/content/browser/gpu/gpu_data_manager_impl_private.h
index 067625a..f6e0679a 100644
--- a/content/browser/gpu/gpu_data_manager_impl_private.h
+++ b/content/browser/gpu/gpu_data_manager_impl_private.h
@@ -85,6 +85,9 @@
   bool DirectXRequested() const;
   bool VulkanRequested() const;
   void TerminateInfoCollectionGpuProcess();
+  void SetUseAdapterLuid(const CHROME_LUID& luid);
+  void ClearUseAdapterLuid();
+  std::optional<CHROME_LUID> GetUseAdapterLuid() const;
 #endif
   void PostCreateThreads();
   void UpdateDawnInfo(const std::vector<std::string>& dawn_info_list);
@@ -259,6 +262,7 @@
   bool gpu_info_vulkan_valid_ = false;
   bool gpu_info_vulkan_requested_ = false;
   bool gpu_info_vulkan_request_failed_ = false;
+  std::optional<CHROME_LUID> use_adapter_luid_;
 #endif
   // The Dawn info queried from the GPU process.
   std::vector<std::string> dawn_info_list_;
diff --git a/content/browser/gpu/gpu_process_host.cc b/content/browser/gpu/gpu_process_host.cc
index e7d2274..b6ddfa6 100644
--- a/content/browser/gpu/gpu_process_host.cc
+++ b/content/browser/gpu/gpu_process_host.cc
@@ -294,7 +294,6 @@
     switches::kSkiaGraphiteDawnBackend,
     switches::kSkiaResourceCacheLimitMb,
     switches::kTestGLLib,
-    switches::kUseAdapterLuid,
     switches::kUseFakeMjpegDecodeAccelerator,
     switches::kUseGpuInTests,
     switches::kWebViewDrawFunctorUsesVulkan,
diff --git a/content/browser/xr/service/xr_runtime_manager_impl.cc b/content/browser/xr/service/xr_runtime_manager_impl.cc
index 86525f8..672eb48 100644
--- a/content/browser/xr/service/xr_runtime_manager_impl.cc
+++ b/content/browser/xr/service/xr_runtime_manager_impl.cc
@@ -21,6 +21,7 @@
 #include "base/trace_event/trace_event.h"
 #include "base/trace_event/typed_macros.h"
 #include "build/build_config.h"
+#include "content/browser/gpu/gpu_data_manager_impl.h"
 #include "content/browser/xr/service/xr_frame_sink_client_impl.h"
 #include "content/browser/xr/webxr_internals/mojom/webxr_internals.mojom.h"
 #include "content/browser/xr/webxr_internals/webxr_internals_handler_impl.h"
@@ -29,7 +30,6 @@
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/device_service.h"
 #include "content/public/browser/global_routing_id.h"
-#include "content/public/browser/gpu_data_manager.h"
 #include "content/public/browser/gpu_utils.h"
 #include "content/public/browser/xr_runtime_manager.h"
 #include "content/public/common/child_process_id_util.h"
@@ -425,12 +425,9 @@
     // runtime doesn't specify a LUID.
     DCHECK(luid && (luid->HighPart != 0 || luid->LowPart != 0));
 
-    // Add the XR compatible adapter LUID to the browser command line.
-    // GpuProcessHost::LaunchGpuProcess passes this to the GPU process.
-    std::string luid_string = base::NumberToString(luid->HighPart) + "," +
-                              base::NumberToString(luid->LowPart);
-    base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
-        switches::kUseAdapterLuid, luid_string);
+    // Set the XR compatible adapter LUID in GpuDataManager.
+    // GpuDataManagerImpl::AppendGpuCommandLine passes this to the GPU process.
+    content::GpuDataManagerImpl::GetInstance()->SetUseAdapterLuid(*luid);
 
     // Store the current GPU so we can revert back once XR is no longer needed.
     // If default_gpu_ is nonzero, we have already previously stored the
@@ -515,12 +512,12 @@
   CHECK_EQ(g_xr_runtime_manager, this);
   g_xr_runtime_manager = nullptr;
 
-  // If a GPU adapter LUID was added to the command line to pass to the GPU
-  // process, remove the switch so subsequent GPU processes initialize on the
-  // default GPU.
+  // If a GPU adapter LUID was specified for the GPU process, clear it so
+  // subsequent GPU processes initialize on the default GPU.
   if (xr_compatible_restarted_gpu_) {
-    base::CommandLine::ForCurrentProcess()->RemoveSwitch(
-        switches::kUseAdapterLuid);
+#if BUILDFLAG(IS_WIN)
+    content::GpuDataManagerImpl::GetInstance()->ClearUseAdapterLuid();
+#endif
 
     // Ensure this object is no longer registered as a GpuDataManager observer,
     // which may happen if MakeXrCompatible is called and the page is navigated
Loading diff…

Original Bug Report

reported by [email protected]

Potential Browser Process UAF via Unsynchronized Data Race on base::CommandLine Singleton

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 Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A data race exists in the browser process where the global base::CommandLine singleton is mutated on the UI thread via WebXR while being read concurrently on the process-launcher thread. Because base::CommandLine uses unsynchronized std::map and std::vector containers, this concurrent access can lead to a Use-After-Free (UAF) or memory corruption. The vulnerability is reachable from a compromised renderer and impacts the unsandboxed browser process.

Affected files:

  • content/browser/xr/service/xr_runtime_manager_impl.cc
  • base/command_line.cc
  • content/browser/child_process_launcher_helper.cc
  • content/browser/child_process_launcher_helper_win.cc
  • sandbox/policy/win/sandbox_win.cc
  • base/command_line.h

Estimated timestamp from git blame: 2020-06-17

Summary

A data race on the base::CommandLine singleton allows for a potential Use-After-Free (UAF) in the unsandboxed browser process. The race occurs because the global command line is modified on the UI thread (triggered by WebXR Mojo calls) while being accessed on the dedicated process-launcher thread during child process launches.

Root Cause Analysis

In base/command_line.h, command-line switches and arguments are stored in switches_ (std::map) and argv_ (std::vector). These containers are not thread-safe. Although base::CommandLine includes an InstanceBoundSequenceChecker, it is typically compiled out in production builds. Consequently, mutators like AppendSwitch and RemoveSwitch modify the containers without synchronization, which can cause tree rebalancing or node deletions while other threads are traversing the map.

Vulnerability Details

The Writer (UI Thread)

In content/browser/xr/service/xr_runtime_manager_impl.cc, the MakeXrCompatible method appends a switch to the global command line when a compatible GPU adapter LUID is identified:

base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
    switches::kUseAdapterLuid, luid_string);

Additionally, the destructor XRRuntimeManagerImpl::~XRRuntimeManagerImpl removes this switch:

base::CommandLine::ForCurrentProcess()->RemoveSwitch(switches::kUseAdapterLuid);

The Reader (Launcher Thread)

Every child process launch (renderer, GPU, utility, etc.) involves reading the global CommandLine singleton on a dedicated thread pool thread (the launcher thread). Functions like internal::ChildProcessLauncherHelper::LaunchOnLauncherThread perform multiple reads using HasSwitch or GetSwitchValueASCII (e.g., in content/browser/child_process_launcher_helper.cc at line 315).

Impact

Concurrent mutation and traversal of a std::map is undefined behavior. In libc++, this can cause a reader thread to follow pointers of nodes that are being freed or rotated, leading to a browser-process UAF. Because libc++ tree nodes and std::string buffers use bare pointers rather than raw_ptr<>, MiraclePtr does not provide protection for this specific case. A UAF in the browser process is a critical security issue that could lead to a sandbox escape.

Suggested Reproductions Steps (Potential)

  1. From a compromised renderer, repeatedly invoke navigator.xr.makeXRCompatible() and immediately drop the Mojo connection to trigger frequent AppendSwitch and RemoveSwitch calls on the UI thread.
  2. Simultaneously, trigger frequent child process launches (e.g., by creating many cross-site iframes) to force concurrent reads of the CommandLine singleton on the launcher thread.
  3. Observe for memory corruption or crashes in the browser process resulting from the data race on the switches_ map.

Suggested Fix

Operations on the global base::CommandLine singleton should be synchronized using a lock, or the singleton should be made immutable after initialization. If runtime mutations are required, they should be performed on a thread-safe wrapper or protected by a global mutex.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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.

View on issue tracker