CVE-2026-79125
Overview
Background
- `VRServiceImpl`
- the browser-process implementation of the WebXR
VRServiceMojo interface, one perRenderFrameHost, that mediates a frame’s access to XR runtimes/devices. - `RuntimesChanged()` / `OnDeviceChanged()`
- the hot-plug notification path: when an XR runtime is added/removed,
RuntimesChanged()fires and forwardsOnDeviceChanged()to the renderer’sservice_client_. - Frame visibility (`PageVisibilityState::kVisible`)
- whether the
RenderFrameHostassociated with the service is currently visible; background/occluded/hidden frames are non-visible. - `WebContentsObserver::OnVisibilityChanged`
- the observer callback invoked when the associated
WebContents/page visibility transitions.
Root Cause Analysis
VRServiceImpl::RuntimesChanged() forwarded XR runtime hot-plug events to the renderer via service_client_->OnDeviceChanged() unconditionally, without checking whether the service’s RenderFrameHost was visible. This let a hidden or backgrounded frame continue to observe real-time XR device connect/disconnect events, violating the platform-sensor invariant (already enforced for sensors such as DevicePostureProviderImpl) that such events are only delivered to visible frames.
The fix adds an IsRenderFrameHostVisible() helper and, in RuntimesChanged(), defers delivery when the frame is not visible by setting pending_device_changed_ = true and returning early. It then overrides OnVisibilityChanged() to flush a single deferred OnDeviceChanged() once the frame becomes visible again, re-checking visibility because the frame may stay hidden even if the top-level page becomes visible. The existing session-request path was also refactored to use the new IsRenderFrameHostVisible() helper in place of an inline GetVisibilityState() comparison.
IsRenderFrameHostVisible() and defers/flushes it via pending_device_changed_ and OnVisibilityChanged().Attack Path
- Load a page in a non-visible frame
An attacker page runs in a hidden/backgrounded context (e.g. a background tab or an occluded/hidden frame) that still holds a
VRServiceconnection. - XR runtime hot-plug occurs
An XR device is connected or disconnected system-wide, causing
RuntimesChanged()to fire in the browser process. - Event delivered to the hidden frame (pre-patch)
RuntimesChanged()callsservice_client_->OnDeviceChanged()without a visibility check, so the hidden frame is informed of the device change. - Cross-context inference The hidden frame learns of XR hardware presence/connection state changes it should not observe while non-visible, yielding an information leak / side channel about the user’s devices (correlating device state across contexts is inferred).
Impact Assessment
VRService. The precondition is merely holding a VRService connection while non-visible, which requires no special privilege. The vendor advisory rates this low severity and does not report in-the-wild exploitation; NVD scores it CVSS 6.5.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/xr/service/vr_service_impl.cc |
modified |
Files Changed
content/browser/xr/service/vr_service_impl.hcontent/browser/xr/service/vr_service_impl.cc
Audit Directions
- Sensor/device events without visibility gatingAudit other browser-process Mojo services that push hardware or environment change events to renderers (posture, ambient light, geolocation, gamepad, device orientation) to confirm they gate delivery on frame visibility as
DevicePostureProviderImpldoes. - Deferred-then-flush patternsReview
OnVisibilityChanged/focus flush handlers that replay a pending event, ensuring visibility is re-checked at flush time (frame may stay hidden while the page becomes visible) and that only a bounded single notification is coalesced. - Hidden-frame observabilityLook for
WebContentsObserver-backed services that continue emitting to the renderer for background/occluded frames, which can serve as cross-tab side channels.
Patch
From 86ca49b1d3680f6c9c60ab20d59440b3810df1f8 Mon Sep 17 00:00:00 2001 From: Alexander Cooper <[email protected]> Date: Mon, 13 Jul 2026 11:08:52 -0700 Subject: [PATCH] Gate VRService RuntimesChanged on frame visibility VRServiceImpl previously forwarded XR runtime hot-plug events via RuntimesChanged() without checking if the associated RenderFrameHost was visible. To align with other platform sensors (e.g., DevicePostureProviderImpl) and ensure events are only delivered to visible frames, this change: - Adds an IsRenderFrameHostVisible() helper method to VRServiceImpl. - Checks frame visibility in RuntimesChanged() and sets a pending_device_changed_ flag if the frame is not currently visible. - Overrides WebContentsObserver::OnVisibilityChanged() to flush any deferred device change notifications once the associated frame becomes visible again. TAG=agy Fixed: 533001362 Change-Id: I405b72ce431b100eed229996c48911cce192a617 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8077558 Reviewed-by: Brandon Jones <[email protected]> Commit-Queue: Alexander Cooper <[email protected]> Commit-Queue: Brandon Jones <[email protected]> Auto-Submit: Alexander Cooper <[email protected]> Cr-Commit-Position: refs/heads/main@{#1661243} --- diff --git a/content/browser/xr/service/vr_service_impl.cc b/content/browser/xr/service/vr_service_impl.cc index 2fde838f..8f3cf7f 100644 --- a/content/browser/xr/service/vr_service_impl.cc +++ b/content/browser/xr/service/vr_service_impl.cc @@ -274,6 +274,10 @@ void VRServiceImpl::RuntimesChanged() { DVLOG(2) << __func__; + if (!IsRenderFrameHostVisible()) { + pending_device_changed_ = true; + return; + } if (service_client_) { service_client_->OnDeviceChanged(); } @@ -303,6 +307,18 @@ receiver_->Close(); } +void VRServiceImpl::OnVisibilityChanged(content::Visibility visibility) { + // Re-check frame visibility, as our associated RenderFrameHost may remain + // hidden even when the top-level page becomes visible. + if (!pending_device_changed_ || !IsRenderFrameHostVisible()) { + return; + } + pending_device_changed_ = false; + if (service_client_) { + service_client_->OnDeviceChanged(); + } +} + void VRServiceImpl::OnWebContentsFocusChanged(content::RenderWidgetHost* host, bool focused) { if (!render_frame_host_ || !render_frame_host_->GetView() || @@ -550,8 +566,7 @@ return; } - if (render_frame_host_->GetVisibilityState() != - content::PageVisibilityState::kVisible) { + if (!IsRenderFrameHostVisible()) { // Page visibility is verified blink-side, so this should never fail unless // the requesting client is misbehaving or compromised. Treat non-visible // page as unknown failure: @@ -997,6 +1012,11 @@ return content::WebContents::FromRenderFrameHost(render_frame_host_); } +bool VRServiceImpl::IsRenderFrameHostVisible() const { + return render_frame_host_ && render_frame_host_->GetVisibilityState() == + content::PageVisibilityState::kVisible; +} + void VRServiceImpl::Teardown() { if (!render_frame_host_) { return; diff --git a/content/browser/xr/service/vr_service_impl.h b/content/browser/xr/service/vr_service_impl.h index 65bd2a02..f36d92b 100644 --- a/content/browser/xr/service/vr_service_impl.h +++ b/content/browser/xr/service/vr_service_impl.h @@ -14,6 +14,7 @@ #include "content/browser/xr/metrics/session_metrics_helper.h" #include "content/common/content_export.h" #include "content/public/browser/permission_result.h" +#include "content/public/browser/visibility.h" #include "content/public/browser/web_contents_observer.h" #include "content/public/browser/xr_install_helper.h" #include "device/vr/public/mojom/isolated_xr_service.mojom-forward.h" @@ -126,6 +127,7 @@ void OnWebContentsFocused(content::RenderWidgetHost* host) override; void OnWebContentsLostFocus(content::RenderWidgetHost* host) override; void RenderFrameDeleted(content::RenderFrameHost* host) override; + void OnVisibilityChanged(content::Visibility visibility) override; void OnWebContentsFocusChanged(content::RenderWidgetHost* host, bool focused); @@ -137,6 +139,8 @@ // assumption that we are not already in VR. SessionMetricsHelper* GetSessionMetricsHelper(); + bool IsRenderFrameHostVisible() const; + bool InternalSupportsSession(device::mojom::XRSessionOptions* options); void DoRequestPermissions( @@ -206,6 +210,7 @@ bool in_focused_frame_ = false; bool frames_throttled_ = false; bool has_immersive_session_ = false; + bool pending_device_changed_ = false; std::vector<XrCompatibleCallback> xr_compatible_callbacks_;