CVE-2026-9118
Overview
Background
- `XRRuntimeManagerImpl`
- the browser-process object that coordinates WebXR runtimes and, during a GPU adapter switch, registers as a
GpuDataManagerobserver. - `GpuDataManagerObserver`
- an observer interface whose
OnGpuInfoUpdate()is invoked when the GPU process reports new GPU info after a restart. - Use-after-free
- a dangling observer pointer that is dispatched to a virtual method after its backing object has been destroyed.
Root Cause Analysis
XRRuntimeManagerImpl::MakeXrCompatible() registers the object as a GpuDataManager observer (described in the report; the AddObserver(this) call site is not shown in the material) so that it is notified when the GPU process restarts. The only shown unregistration happens in XRRuntimeManagerImpl::OnGpuInfoUpdate() at content/browser/xr/service/xr_runtime_manager_impl.cc:482, which calls content::GpuDataManager::GetInstance()->RemoveObserver(this) once the restart completes. In the pre-fix destructor XRRuntimeManagerImpl::~XRRuntimeManagerImpl() (xr_runtime_manager_impl.cc:513-538), the only cleanup guarded by if (xr_compatible_restarted_gpu_) (:521) is removing the kUseAdapterLuid switch and, on Windows, reverting the GPU — there is no RemoveObserver(this) call. Thus if the object is destroyed after MakeXrCompatible() registered it but before OnGpuInfoUpdate() fired, it remains registered as an observer while its memory is freed, violating the invariant that an observer outlives its registration.
The fix inserts content::GpuDataManager::GetInstance()->RemoveObserver(this); into that destructor block (diff added lines at pre-fix :523-524), ensuring the observer registration is always torn down when the object dies.
OnGpuInfoUpdate()), not in its destructor, so an early teardown left a dangling observer; the patch adds the missing RemoveObserver(this) to the destructor.Attack Path
- Register observer
A page triggers the WebXR GPU-compatibility restart flow, causing
XRRuntimeManagerImplto register itself as aGpuDataManagerobserver. (inferred: the registeringAddObservercall is not in the shown material.) - Destroy early
The page is navigated/torn down so
XRRuntimeManagerImplis destroyed before the GPU process finishes restarting, and the pre-fix destructor never callsRemoveObserver(this). - Stale dispatch
When the GPU process later restarts,
GpuDataManagerdispatchesOnGpuInfoUpdate()on the freed object. (inferred: the dispatch mechanism is described by the reporter, not shown in the code.)
Impact Assessment
GpuDataManagerObserver in the browser process that is later invoked, a use-after-free in the privileged browser process. The advisory rates this high severity and reports no in-the-wild exploitation; any escalation to code execution is (inferred).Files Changed
content/browser/xr/service/xr_runtime_manager_impl.cc
Audit Directions
- Observer lifetimeAny class that calls
AddObserverconditionally must guarantee a matchingRemoveObserveron every destruction path, not only on the expected callback. - ScopedObservationPrefer
base::ScopedObservation/base::ScopedObservationTraitsso registration is tied to member lifetime rather than manual add/remove pairs. - Async completion racesReview objects that register for an asynchronous event (GPU restart, process relaunch) and can be destroyed before that event arrives.
Patch
From 7adec41c4fdc3e95411efb45b7c263b8ecc43496 Mon Sep 17 00:00:00 2001
From: Brandon Jones <bajones@chromium.org>
Date: Tue, 07 Apr 2026 11:21:13 -0700
Subject: [PATCH] Remove GPU observer in XRRuntimeManager destructor
Ensures that the XRRuntimeManager is removed as a GpuDataManager
observer in the XRRuntimeManager destructor if it was ever registered as
one. Normally it is removed when the GPU process restarts, but this is a
safety precaution to prevent errors if the XRRuntimeManager is destroyed
while the process is still restarting.
Bug: 498702233
Change-Id: Iaf27dee1eaad2e228e69c98d9434a2d4fe85a308
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7729684
Reviewed-by: Brian Sheedy <bsheedy@chromium.org>
Commit-Queue: Brandon Jones <bajones@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1610896}
---
diff --git a/content/browser/xr/service/xr_runtime_manager_impl.cc b/content/browser/xr/service/xr_runtime_manager_impl.cc
index cfb0f2d..b8fe8a1 100644
--- a/content/browser/xr/service/xr_runtime_manager_impl.cc
+++ b/content/browser/xr/service/xr_runtime_manager_impl.cc
@@ -522,6 +522,11 @@
base::CommandLine::ForCurrentProcess()->RemoveSwitch(
switches::kUseAdapterLuid);
+ // Ensure this object is no longer registered as a GpuDataManager observer,
+ // which may happen if MakeXrCompatible is called and the page is navigated
+ // before the GPU process restarts.
+ content::GpuDataManager::GetInstance()->RemoveObserver(this);
+
#if BUILDFLAG(IS_WIN)
// If we changed the GPU, revert it back to the default GPU. This is
// separate from xr_compatible_restarted_gpu_ because the GPU process may
Original Bug Report
Potential Use-After-Free in XRRuntimeManagerImpl during GPU Restart
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 Use-After-Free vulnerability exists in the browser process due to XRRuntimeManagerImpl failing to unregister itself as a GpuDataManagerObserver when destroyed. If the object is destroyed before the GPU process restarts, a dangling pointer remains in GpuDataManager, leading to an attacker-controlled virtual method call when the GPU eventually restarts.
Affected files:
content/browser/xr/service/xr_runtime_manager_impl.cc
Estimated timestamp from git blame: 2022-01-15
Summary
A Use-After-Free (UAF) vulnerability exists in content::XRRuntimeManagerImpl within the browser process. During the WebXR MakeXrCompatible flow (typically on Windows multi-GPU systems), the class registers itself as a content::GpuDataManagerObserver to monitor when the GPU process restarts. However, it fails to unregister itself in its destructor. If the XRRuntimeManagerImpl is destroyed before the GPU process finishes restarting, a dangling pointer is left in the observer list, which is subsequently used for a virtual method dispatch, potentially leading to Remote Code Execution (RCE).
Vulnerability Details
In content/browser/xr/service/xr_runtime_manager_impl.cc, the MakeXrCompatible() method handles requests to restart the GPU process with a specific adapter. It adds itself as an observer to GpuDataManager to be notified when the new GPU process is ready:
void XRRuntimeManagerImpl::MakeXrCompatible() {
// ...
xr_compatible_restarted_gpu_ = true;
content::GpuDataManager::GetInstance()->AddObserver(this);
content::KillGpuProcess();
return;
}
The intended cleanup happens when the GPU process restarts and sends its info:
void XRRuntimeManagerImpl::OnGpuInfoUpdate() {
content::GpuDataManager::GetInstance()->RemoveObserver(this);
// ...
}
XRRuntimeManagerImpl is a ref-counted object whose lifetime is primarily managed by active VRServiceImpl instances (one per WebXR frame). If an attacker triggers gl.makeXRCompatible() and immediately navigates away from the page, the VRServiceImpl is destroyed. If this was the last active WebXR session, the XRRuntimeManagerImpl ref-count drops to zero, and it is destroyed.
The destructor (~XRRuntimeManagerImpl) fails to call content::GpuDataManager::GetInstance()->RemoveObserver(this).
GpuDataManager tracks its observers using base::ObserverListThreadSafe<GpuDataManagerObserver>. This class stores observers as raw pointers (ObserverType*) in an internal std::unordered_map. Because it’s a raw C++ pointer and not a base::raw_ptr, Chrome’s MiraclePtr (BackupRefPtr) mitigation does not quarantine the freed XRRuntimeManagerImpl memory.
When the GPU process eventually restarts (which can take hundreds of milliseconds), ObserverListThreadSafe notifies its observers. It extracts the dangling bare pointer from its map and posts a task to the UI thread to call the virtual method OnGpuInfoUpdate().
Potential Exploit Scenario
Note: The following steps describe a potential attack path. We do not have a working proof of concept.
- Attacker Setup: A malicious web page on a system that requires a GPU adapter switch for XR (e.g., a Windows laptop with integrated and discrete GPUs) calls
gl.makeXRCompatible(). - Observer Registration: The browser process kills the GPU process and adds the
XRRuntimeManagerImplto theGpuDataManagerobserver list. - Trigger UAF: The malicious page immediately triggers a navigation (e.g.,
window.location = 'about:blank'). This destroys theVRServiceImpland the underlyingXRRuntimeManagerImplobject. - Heap Spray Window: The GPU process takes hundreds of milliseconds to restart. During this window, the attacker uses other browser IPCs (like Blob creation or IndexedDB) to heap-spray the browser process and reclaim the freed
XRRuntimeManagerImplmemory chunk with controlled data, including a fake vtable. - Execution: The GPU process finishes restarting and notifies the browser process.
ObserverListThreadSafeinvokes theOnGpuInfoUpdate()virtual method on the dangling pointer. The C++ runtime performs a virtual method dispatch using the attacker-controlled vtable, leading to arbitrary code execution in the browser process, bypassing the sandbox.
Suggested Fix
The most robust fix is to use base::ScopedObservation to manage the observer registration. This ensures that the observer is automatically unregistered when XRRuntimeManagerImpl is destroyed, even if OnGpuInfoUpdate hasn’t been called yet.
- Add a member to
XRRuntimeManagerImplincontent/browser/xr/service/xr_runtime_manager_impl.h:base::ScopedObservation<content::GpuDataManager, content::GpuDataManagerObserver> gpu_data_manager_observation_{this}; - In
XRRuntimeManagerImpl::MakeXrCompatible(), replacecontent::GpuDataManager::GetInstance()->AddObserver(this);with:gpu_data_manager_observation_.Observe(content::GpuDataManager::GetInstance()); - In
XRRuntimeManagerImpl::OnGpuInfoUpdate(), replacecontent::GpuDataManager::GetInstance()->RemoveObserver(this);with:gpu_data_manager_observation_.Reset();
Evaluated with Chrome root at commit: e9e0fcbb690b1a8c1a26c81c2a9ea23d6e178368
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.