Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in WebXR
DescriptionInappropriate implementation in WebXR
ComponentWebXR
Bug ClassLogic Error
Tracker496368832
Fix commit000d6aa1c796 (chromium/src) +150/-66
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
switch
components/webxr/android/arcore_install_helper.cc
modified
if
components/webxr/android/arcore_install_helper.cc
modified
if
content/browser/xr/service/browser_xr_runtime_impl.cc
modified

Files Changed

  • components/webxr/android/arcore_install_helper.cc
  • components/webxr/android/arcore_install_helper.h
  • content/browser/permissions/permission_util.cc
  • content/browser/xr/service/browser_xr_runtime_impl.cc
From 000d6aa1c796a98a0f31adf3ee3201796ad3d746 Mon Sep 17 00:00:00 2001
From: Alexander Cooper <[email protected]>
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 <[email protected]>
Reviewed-by: Ravjit Uppal <[email protected]>
Commit-Queue: Alexander Cooper <[email protected]>
Reviewed-by: Brandon Jones <[email protected]>
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.