High watch ✔︎ verified CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
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 — factors set by independent review by AI
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.
Independent review by AIConfirmed
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker498702233
Fix commit7adec41c4fdc (chromium/src) +5/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-14 (per vendor advisory)
Fix landed2026-04-07 (public git)
Disclosed2026-05-19
Patch gap42 days — fix public in source before the advisory

Background

`XRRuntimeManagerImpl`
the browser-process object that coordinates WebXR runtimes and, during a GPU adapter switch, registers as a GpuDataManager observer.
`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.

Key insight
The object unregistered itself only on the success path (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

  1. Register observer A page triggers the WebXR GPU-compatibility restart flow, causing XRRuntimeManagerImpl to register itself as a GpuDataManager observer. (inferred: the registering AddObserver call is not in the shown material.)
  2. Destroy early The page is navigated/torn down so XRRuntimeManagerImpl is destroyed before the GPU process finishes restarting, and the pre-fix destructor never calls RemoveObserver(this).
  3. Stale dispatch When the GPU process later restarts, GpuDataManager dispatches OnGpuInfoUpdate() on the freed object. (inferred: the dispatch mechanism is described by the reporter, not shown in the code.)

Impact Assessment

An attacker-reachable teardown leaves a dangling 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 lifetime
    Any class that calls AddObserver conditionally must guarantee a matching RemoveObserver on every destruction path, not only on the expected callback.
  • ScopedObservation
    Prefer base::ScopedObservation/base::ScopedObservationTraits so registration is tied to member lifetime rather than manual add/remove pairs.
  • Async completion races
    Review objects that register for an asynchronous event (GPU restart, process relaunch) and can be destroyed before that event arrives.
From 7adec41c4fdc3e95411efb45b7c263b8ecc43496 Mon Sep 17 00:00:00 2001
From: Brandon Jones <[email protected]>
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 <[email protected]>
Commit-Queue: Brandon Jones <[email protected]>
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
Loading diff…

Original Bug Report

reported by [email protected]

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.

  1. 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().
  2. Observer Registration: The browser process kills the GPU process and adds the XRRuntimeManagerImpl to the GpuDataManager observer list.
  3. Trigger UAF: The malicious page immediately triggers a navigation (e.g., window.location = 'about:blank'). This destroys the VRServiceImpl and the underlying XRRuntimeManagerImpl object.
  4. 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 XRRuntimeManagerImpl memory chunk with controlled data, including a fake vtable.
  5. Execution: The GPU process finishes restarting and notifies the browser process. ObserverListThreadSafe invokes the OnGpuInfoUpdate() 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.

  1. Add a member to XRRuntimeManagerImpl in content/browser/xr/service/xr_runtime_manager_impl.h:
    base::ScopedObservation<content::GpuDataManager, content::GpuDataManagerObserver>
        gpu_data_manager_observation_{this};
    
  2. In XRRuntimeManagerImpl::MakeXrCompatible(), replace content::GpuDataManager::GetInstance()->AddObserver(this); with:
    gpu_data_manager_observation_.Observe(content::GpuDataManager::GetInstance());
    
  3. In XRRuntimeManagerImpl::OnGpuInfoUpdate(), replace content::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.

View on issue tracker