CVE-2026-9118
Overview
Files Changed
content/browser/xr/service/xr_runtime_manager_impl.cc
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.