Chrome · WebXR
CVE-2026-14034
Logic Error in WebXR
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchcomponents/webxr/android/arcore_install_helper.cc |
modified | |
ifcomponents/webxr/android/arcore_install_helper.cc |
modified | |
ifcontent/browser/xr/service/browser_xr_runtime_impl.cc |
modified |
Files Changed
components/webxr/android/arcore_install_helper.cccomponents/webxr/android/arcore_install_helper.hcontent/browser/permissions/permission_util.cccontent/browser/xr/service/browser_xr_runtime_impl.cc
Patch
From 000d6aa1c796a98a0f31adf3ee3201796ad3d746 Mon Sep 17 00:00:00 2001
From: Alexander Cooper <alcooper@chromium.org>
Date: Thu, 07 May 2026 13:11:15 -0700
Subject: [PATCH] [WebXR] Ensure user activation is only extended when needed
Currently, VRServiceImpl unconditionally extends transient user
activation upon permission resolution and runtime installation, even if
these occurred silently/synchronously without requiring user
interaction.
This CL ensures that user activation is only extended if a permission
prompt was actually shown to the user or if the runtime installation
displayed UI that required user interaction.
To achieve this:
1. Introducing content::XrInstallResult to distinguish between
successful installation that showed UI (kSuccessInstalled),
already installed runtime with no UI (kSuccessAlreadyInstalled),
and failure (kFailed).
2. Refactoring BrowserXRRuntimeImpl and ArCoreInstallHelper to return
the appropriate XrInstallResult.
3. Refactoring VRServiceImpl::DoRequestPermissions to check the
permission status of all requested permissions upfront. If no
permissions require a prompt (status is not ASK), it short-circuits
and avoids the asynchronous IPC, passing needs_prompt = false.
Note that this does need to check device-side permissions and so the
appropriate permission_util class was updated to allow the WebXR
permissions that need device permissions to be queried, since this
method was only used in this type of stack.
4. Only calling NotifyUserActivation() in VRServiceImpl if a
permission prompt or installation UI was actually shown.
Bug: 496368832
Change-Id: I2b3c9fffe0f080918bf87a5da4f3957ad75498eb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7819354
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Reviewed-by: Ravjit Uppal <ravjit@chromium.org>
Commit-Queue: Alexander Cooper <alcooper@chromium.org>
Reviewed-by: Brandon Jones <bajones@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1627181}
---
diff --git a/components/webxr/android/arcore_install_helper.cc b/components/webxr/android/arcore_install_helper.cc
index d87882b..b4a9ccab 100644
--- a/components/webxr/android/arcore_install_helper.cc
+++ b/components/webxr/android/arcore_install_helper.cc
@@ -54,12 +54,12 @@
java_install_utils_);
}
- RunInstallFinishedCallback(false);
+ RunInstallFinishedCallback(content::XrInstallResult::kFailed);
}
void ArCoreInstallHelper::EnsureInstalled(
const content::GlobalRenderFrameHostId& frame_id,
- base::OnceCallback<void(bool)> install_callback) {
+ base::OnceCallback<void(content::XrInstallResult)> install_callback) {
DVLOG(1) << __func__ << ": java_install_utils_.is_null()="
<< java_install_utils_.is_null();
@@ -67,7 +67,7 @@
install_finished_callback_ = std::move(install_callback);
if (java_install_utils_.is_null()) {
- RunInstallFinishedCallback(false);
+ RunInstallFinishedCallback(content::XrInstallResult::kFailed);
return;
}
@@ -78,9 +78,9 @@
return;
}
- // ARCore did not need to be installed/updated so mock out that its
- // installation succeeded.
- OnRequestInstallSupportedArCoreResult(nullptr, true);
+ // ARCore did not need to be installed/updated.
+ RunInstallFinishedCallback(
+ content::XrInstallResult::kSuccessAlreadyInstalled);
}
void ArCoreInstallHelper::ShowMessage(
@@ -93,7 +93,7 @@
int button_text = -1;
switch (availability) {
case ArCoreAvailability::kUnsupportedDeviceNotCapable: {
- RunInstallFinishedCallback(false);
+ RunInstallFinishedCallback(content::XrInstallResult::kFailed);
return; // No need to process further
}
case ArCoreAvailability::kUnknownChecking:
@@ -163,12 +163,15 @@
DVLOG(1) << __func__;
// Nothing else to do, simply call the deferred callback.
- RunInstallFinishedCallback(success);
+ RunInstallFinishedCallback(success
+ ? content::XrInstallResult::kSuccessInstalled
+ : content::XrInstallResult::kFailed);
}
-void ArCoreInstallHelper::RunInstallFinishedCallback(bool succeeded) {
+void ArCoreInstallHelper::RunInstallFinishedCallback(
+ content::XrInstallResult result) {
if (install_finished_callback_) {
- std::move(install_finished_callback_).Run(succeeded);
+ std::move(install_finished_callback_).Run(result);
}
}
diff --git a/components/webxr/android/arcore_install_helper.h b/components/webxr/android/arcore_install_helper.h
index b3c2982..df75942 100644
--- a/components/webxr/android/arcore_install_helper.h
+++ b/components/webxr/android/arcore_install_helper.h
@@ -44,9 +44,9 @@
ArCoreInstallHelper& operator=(const ArCoreInstallHelper&) = delete;
// content::XrInstallHelper implementation.
- void EnsureInstalled(
- const content::GlobalRenderFrameHostId& frame_id,
- base::OnceCallback<void(bool)> install_callback) override;
+ void EnsureInstalled(const content::GlobalRenderFrameHostId& frame_id,
+ base::OnceCallback<void(content::XrInstallResult)>
+ install_callback) override;
// Called from Java end.
void OnRequestInstallSupportedArCoreResult(JNIEnv* env, bool success);
@@ -56,9 +56,9 @@
void HandleMessagePrimaryAction(
const content::GlobalRenderFrameHostId& frame_id);
void HandleMessageDismissed(messages::DismissReason dismiss_reason);
- void RunInstallFinishedCallback(bool succeeded);
+ void RunInstallFinishedCallback(content::XrInstallResult result);
- base::OnceCallback<void(bool)> install_finished_callback_;
+ base::OnceCallback<void(content::XrInstallResult)> install_finished_callback_;
base::android::ScopedJavaGlobalRef<jobject> java_install_utils_;
std::unique_ptr<messages::MessageWrapper> message_;
diff --git a/content/browser/permissions/permission_util.cc b/content/browser/permissions/permission_util.cc
index c03cac1..23ae649 100644
--- a/content/browser/permissions/permission_util.cc
+++ b/content/browser/permissions/permission_util.cc
@@ -142,7 +142,10 @@
const blink::mojom::PermissionDescriptorPtr& descriptor) {
return descriptor->name == blink::mojom::PermissionName::VIDEO_CAPTURE ||
descriptor->name == blink::mojom::PermissionName::AUDIO_CAPTURE ||
- descriptor->name == blink::mojom::PermissionName::GEOLOCATION;
+ descriptor->name == blink::mojom::PermissionName::GEOLOCATION ||
+ descriptor->name == blink::mojom::PermissionName::AR ||
+ descriptor->name == blink::mojom::PermissionName::VR ||
+ descriptor->name == blink::mojom::PermissionName::HAND_TRACKING;
}
bool PermissionUtil::IsEmbeddablePermission(
diff --git a/content/browser/xr/service/browser_xr_runtime_impl.cc b/content/browser/xr/service/browser_xr_runtime_impl.cc
index 5ea6411c..c8b538a 100644
--- a/content/browser/xr/service/browser_xr_runtime_impl.cc
+++ b/content/browser/xr/service/browser_xr_runtime_impl.cc
@@ -149,7 +149,7 @@
}
if (install_finished_callback_) {
- std::move(install_finished_callback_).Run(false);
+ std::move(install_finished_callback_).Run(XrInstallResult::kFailed);
}
}
@@ -404,12 +404,12 @@
void BrowserXRRuntimeImpl::EnsureInstalled(
const content::GlobalRenderFrameHostId& frame_id,
- base::OnceCallback<void(bool)> install_callback) {
+ base::OnceCallback<void(XrInstallResult)> install_callback) {
DVLOG(2) << __func__;
// If there's no install helper, then we can assume no install is needed.
if (!install_helper_) {
- std::move(install_callback).Run(true);
+ std::move(install_callback).Run(XrInstallResult::kSuccessAlreadyInstalled);
return;
}
@@ -417,7 +417,7 @@
bool had_outstanding_callback = false;
if (install_finished_callback_) {
had_outstanding_callback = true;
- std::move(install_finished_callback_).Run(false);
+ std::move(install_finished_callback_).Run(XrInstallResult::kFailed);
}
install_finished_callback_ = std::move(install_callback);
@@ -432,10 +432,10 @@
weak_ptr_factory_.GetWeakPtr()));
}
-void BrowserXRRuntimeImpl::OnInstallFinished(bool succeeded) {
+void BrowserXRRuntimeImpl::OnInstallFinished(XrInstallResult result) {
DCHECK(install_finished_callback_);
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page