High watch CVSS 9.6 chrome UAF 🔧 Commit mapped

Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Bluetooth
DescriptionUse after free in Bluetooth
ComponentBluetooth
Sub-componentBluetooth
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker556926296
Fix commitbf3ee8d08467 (chromium/src) +94/-10
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-09-04 (per vendor advisory)
Fix landed2026-09-16 (public git)
Disclosed2026-09-29
Patch gap13 days — fix public in source before the advisory

Background

`WebBluetoothServiceImpl`
the browser-process document service backing the Web Bluetooth API for a given RenderFrameHost, destroyed when its hosting WebContents/frame goes away.
`BluetoothDeviceChooserController` / `device_chooser_controller_`
the object (held by std::unique_ptr in WebBluetoothServiceImpl) that owns the device-chooser UI; resetting it synchronously closes the chooser bubble widget.
Activation/focus observer fan-out
closing a widget can synchronously notify focus/activation observers, which in some window topologies destroy the hosting WebContents (and thus the document service).
`base::WeakPtr` liveness check
a guard that detects whether this was destroyed during a nested synchronous call before continuing to touch members.

Root Cause Analysis

In WebBluetoothServiceImpl::RequestDeviceImpl, a new requestDevice() call closes any previously-open chooser by calling device_chooser_controller_.reset() before constructing a fresh controller. Resetting the unique_ptr synchronously closes the chooser bubble widget, and in topologies where widget closure triggers focus/activation observers that synchronously tear down the hosting WebContents, WebBluetoothServiceImpl (i.e. this) is destroyed inside the reset() call. The pre-patch code then continued to construct the new BluetoothDeviceChooserController and access members of the now-freed this, a use-after-free.

The fix captures weak_ptr_factory_.GetWeakPtr() before the reset and returns immediately if !weak_this after it, matching the WeakPtr-recheck pattern used in sibling document services. It additionally annotates web_bluetooth_service_ and render_frame_host_ in BluetoothDeviceChooserController with DisableDanglingPtrDetection, since destroying chooser_ during member destruction can synchronously destroy the WebContents and legitimately leave those pointers dangling (they are not dereferenced afterward).

Key insight
The core mistake was assuming this survives device_chooser_controller_.reset(), when synchronous widget-close observer fan-out can destroy the hosting WebContents and the service mid-method; a base::WeakPtr liveness recheck after the reset makes the method bail out instead of touching freed memory.

Attack Path

  1. Open a chooser A page (with the required user activation) calls requestDevice(), opening the Bluetooth device chooser and creating device_chooser_controller_.
  2. Trigger a second request A duplicate/subsequent requestDevice() call arrives while the chooser is open, entering RequestDeviceImpl, which calls device_chooser_controller_.reset() to close the old chooser first.
  3. Synchronous self-destruction Closing the chooser widget fires activation/focus observers that, in the vulnerable window topology, synchronously destroy the hosting WebContents and therefore WebBluetoothServiceImpl (this).
  4. Use-after-free Control returns into RequestDeviceImpl, which (pre-patch) constructs the new controller and accesses freed members of this, dereferencing freed memory in the browser process. Any escalation beyond the UAF is (inferred).

Impact Assessment

A compromised or malicious page that can open and then re-trigger the Bluetooth chooser can cause a use-after-free of WebBluetoothServiceImpl in the browser process under specific window topologies where widget closure destroys the hosting WebContents. Preconditions include Web Bluetooth being reachable and the activation-observer teardown behavior; the advisory classifies this as a use-after-free in Bluetooth and does not report exploited-in-the-wild status, and any memory-corruption primitive beyond the crash is (inferred).

Changed Functions

FunctionChangeNotes
if
content/browser/bluetooth/web_bluetooth_service_impl.cc
modified
WebContentsDestroyingChooser
content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
modified
if
content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
modified
TestBluetoothDelegate
content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
modified
TestContentBrowserClient
content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
modified
TEST_F
content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
modified
BindLambdaForTesting
content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
modified

Files Changed

  • content/browser/bluetooth/bluetooth_device_chooser_controller.h
  • content/browser/bluetooth/web_bluetooth_service_impl.cc
  • content/browser/bluetooth/web_bluetooth_service_impl.h
  • content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc

Audit Directions

  • Reset/destroy of UI-owning members before touching `this`
    Audit methods that reset() or destroy widget/chooser/bubble controllers and then continue to access this; require a WeakPtr recheck or reordering so no member access follows a potentially self-destroying call.
  • Synchronous observer fan-out from widget closure
    Look for widget/window close paths whose focus/activation observers can synchronously destroy WebContents, and verify all callers on those stacks are re-entrancy-safe.
  • Document-service self-destruction patterns
    Compare with sibling document services (e.g. other permission/chooser services) that already use the GetWeakPtr-then-recheck idiom, and flag any that reset controllers without it.
From bf3ee8d084678a53557bcacc22406970e1db479b Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
Date: Wed, 16 Sep 2026 16:01:56 -0700
Subject: [PATCH] bluetooth: Guard against synchronous destruction in RequestDeviceImpl

In WebBluetoothServiceImpl::RequestDeviceImpl, resetting
device_chooser_controller_ synchronously closes any open chooser bubble
widget. In topologies where widget closure and the resulting focus or
activation observer fan-out synchronously destroy the hosting
WebContents, WebBluetoothServiceImpl is destroyed.

This CL adds a base::WeakPtr liveness check after resetting
device_chooser_controller_ in RequestDeviceImpl before accessing members
of this, matching the pattern in sibling document services.
Additionally, adds DisableDanglingPtrDetection to web_bluetooth_service_
and render_frame_host_ in BluetoothDeviceChooserController since
chooser_ destruction during member destruction may synchronously destroy
the hosting WebContents.

TAG=agy
CONV=dcfecb6a-b053-4247-8094-f7ad5b33ea16

Bug: 556926296
Change-Id: Ifad5b98dea0f50a6a67a398b985ea7fe3edabb29
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8372502
Commit-Queue: Alvin Ji <alvinji@chromium.org>
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1699328}
---
diff --git a/content/browser/bluetooth/bluetooth_device_chooser_controller.h b/content/browser/bluetooth/bluetooth_device_chooser_controller.h
index a9a9dbb..8eaa2978 100644
--- a/content/browser/bluetooth/bluetooth_device_chooser_controller.h
+++ b/content/browser/bluetooth/bluetooth_device_chooser_controller.h
@@ -154,10 +154,15 @@
 
   // The adapter used to get existing devices and start a discovery session.
   scoped_refptr<device::BluetoothAdapter> adapter_;
-  // The WebBluetoothServiceImpl that owns this instance.
-  raw_ptr<WebBluetoothServiceImpl> web_bluetooth_service_;
-  // The RenderFrameHost that owns web_bluetooth_service_.
-  raw_ref<RenderFrameHost> render_frame_host_;
+  // The WebBluetoothServiceImpl that owns this instance, and the
+  // RenderFrameHost that owns `web_bluetooth_service_`.
+  // Destroying `chooser_` during member destruction may synchronously destroy
+  // the hosting WebContents and therefore both `web_bluetooth_service_` and
+  // `render_frame_host_`. This is safe because neither is accessed after
+  // `chooser_` is destroyed.
+  raw_ptr<WebBluetoothServiceImpl, DisableDanglingPtrDetection>
+      web_bluetooth_service_;
+  raw_ref<RenderFrameHost, DisableDanglingPtrDetection> render_frame_host_;
 
   BluetoothDeviceRequestPromptInfo prompt_info_;
 
diff --git a/content/browser/bluetooth/web_bluetooth_service_impl.cc b/content/browser/bluetooth/web_bluetooth_service_impl.cc
index b7b9b355..02d2c2a 100644
--- a/content/browser/bluetooth/web_bluetooth_service_impl.cc
+++ b/content/browser/bluetooth/web_bluetooth_service_impl.cc
@@ -1835,13 +1835,15 @@
     return;
   }
 
-  // Calls to requestDevice() require user activation (user gestures).  We
-  // should close any opened chooser when a duplicate requestDevice call is
-  // made with the same user activation or when any gesture occurs outside
-  // of the opened chooser. This does not happen on all platforms so we
-  // don't DCHECK that the old one is closed.  We destroy the old chooser
-  // before constructing the new one to make sure they can't conflict.
+  // Close any opened chooser before constructing a new one. Destroying the
+  // chooser UI may trigger activation observers that synchronously destroy the
+  // hosting WebContents (and therefore `this`). Guard with a WeakPtr.
+  base::WeakPtr<WebBluetoothServiceImpl> weak_this =
+      weak_ptr_factory_.GetWeakPtr();
   device_chooser_controller_.reset();
+  if (!weak_this) {
+    return;
+  }
 
   device_chooser_controller_ =
       std::make_unique<BluetoothDeviceChooserController>(
diff --git a/content/browser/bluetooth/web_bluetooth_service_impl.h b/content/browser/bluetooth/web_bluetooth_service_impl.h
index f4badc2..899cb302 100644
--- a/content/browser/bluetooth/web_bluetooth_service_impl.h
+++ b/content/browser/bluetooth/web_bluetooth_service_impl.h
@@ -139,6 +139,8 @@
   FRIEND_TEST_ALL_PREFIXES(WebBluetoothServiceImplTest,
                            DestroyedDuringRequestDevice);
   FRIEND_TEST_ALL_PREFIXES(WebBluetoothServiceImplTest,
+                           DestroyedDuringRequestDeviceReset);
+  FRIEND_TEST_ALL_PREFIXES(WebBluetoothServiceImplTest,
                            DestroyedDuringRequestScanningStart);
   FRIEND_TEST_ALL_PREFIXES(WebBluetoothServiceImplTest, PermissionAllowed);
   FRIEND_TEST_ALL_PREFIXES(WebBluetoothServiceImplTest,
diff --git a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
index 1fd65ad..c764d23 100644
--- a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
+++ b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
@@ -402,8 +402,30 @@
   ErrorCallback deferred_start_notification_error_callback_;
 };
 
+class WebContentsDestroyingChooser : public BluetoothChooser {
+ public:
+  explicit WebContentsDestroyingChooser(base::OnceClosure on_destroy)
+      : on_destroy_(std::move(on_destroy)) {}
+  WebContentsDestroyingChooser(const WebContentsDestroyingChooser&) = delete;
+  WebContentsDestroyingChooser& operator=(const WebContentsDestroyingChooser&) =
+      delete;
+  ~WebContentsDestroyingChooser() override {
+    if (on_destroy_) {
+      std::move(on_destroy_).Run();
+    }
+  }
+
+ private:
+  base::OnceClosure on_destroy_;
+};
+
 class TestBluetoothDelegate : public BluetoothDelegate {
  public:
+  using RunBluetoothChooserCallback =
+      base::RepeatingCallback<std::unique_ptr<BluetoothChooser>(
+          RenderFrameHost*,
+          const BluetoothChooser::EventHandler&)>;
+
   TestBluetoothDelegate() = default;
   ~TestBluetoothDelegate() override = default;
   TestBluetoothDelegate(const TestBluetoothDelegate&) = delete;
@@ -425,6 +447,20 @@
     }
   }
 
+  std::unique_ptr<BluetoothChooser> RunBluetoothChooser(
+      RenderFrameHost* frame,
+      const BluetoothChooser::EventHandler& event_handler) override {
+    if (run_bluetooth_chooser_callback_) {
+      return run_bluetooth_chooser_callback_.Run(frame, event_handler);
+    }
+    return nullptr;
+  }
+
+  void set_run_bluetooth_chooser_callback(
+      RunBluetoothChooserCallback callback) {
+    run_bluetooth_chooser_callback_ = std::move(callback);
+  }
+
   std::unique_ptr<BluetoothScanningPrompt> ShowBluetoothScanningPrompt(
       RenderFrameHost* frame,
       const BluetoothScanningPrompt::EventHandler& event_handler) override {
@@ -540,6 +576,7 @@
   base::ObserverList<FramePermissionObserver> observer_list_;
   std::map<std::string, blink::WebBluetoothDeviceId> address_to_id_map_;
   std::map<blink::WebBluetoothDeviceId, std::string> id_to_address_map_;
+  RunBluetoothChooserCallback run_bluetooth_chooser_callback_;
 };
 
 class TestContentBrowserClient : public ContentBrowserClient {
@@ -936,6 +973,44 @@
   loop.RunUntilIdle();
 }
 
+TEST_F(WebBluetoothServiceImplTest, DestroyedDuringRequestDeviceReset) {
+  browser_client_.bluetooth_delegate()->set_run_bluetooth_chooser_callback(
+      base::BindLambdaForTesting(
+          [this](RenderFrameHost* frame,
+                 const BluetoothChooser::EventHandler& event_handler)
+              -> std::unique_ptr<BluetoothChooser> {
+            return std::make_unique<WebContentsDestroyingChooser>(
+                base::BindLambdaForTesting([this]() {
+                  service_ptr_ = nullptr;
+                  DeleteContents();
+                }));
+          }));
+
+  // Initial RequestDevice call opens the chooser.
+  auto options1 = blink::mojom::WebBluetoothRequestDeviceOptions::New();
+  options1->accept_all_devices = true;
+  TestFuture<WebBluetoothResult, blink::mojom::WebBluetoothDevicePtr> future1;
+  service_ptr_->RequestDevice(std::move(options1), future1.GetCallback());
+
+  // A duplicate or subsequent RequestDevice call while the chooser is open
+  // triggers device_chooser_controller_.reset(). In topologies where widget
+  // closure destroys the hosting WebContents, WebBluetoothServiceImpl is
+  // destroyed. Ensure this returns cleanly without UAF.
+  auto options2 = blink::mojom::WebBluetoothRequestDeviceOptions::New();
+  options2->accept_all_devices = true;
+  TestFuture<WebBluetoothResult, blink::mojom::WebBluetoothDevicePtr> future2;
+  service_ptr_->RequestDevice(std::move(options2), future2.GetCallback());
+
+  // The first request was cancelled during chooser closure.
+  ASSERT_TRUE(future1.IsReady());
+  EXPECT_EQ(future1.Get<0>(), WebBluetoothResult::CHOOSER_CANCELLED);
+  EXPECT_FALSE(future1.Get<1>());
+
+  // The second request bailed out due to synchronous destruction; its
+  // callback was dropped without running.
+  EXPECT_FALSE(future2.IsReady());
+}
+
 TEST_F(WebBluetoothServiceImplTest, PermissionAllowed) {
   blink::mojom::WebBluetoothLeScanFilterPtr filter = CreateScanFilter("a", "b");
   std::optional<WebBluetoothServiceImpl::ScanFilters> filters;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
index 1fd65ad..c764d23 100644
--- a/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
+++ b/content/browser/bluetooth/web_bluetooth_service_impl_unittest.cc
@@ -402,8 +402,30 @@
   ErrorCallback deferred_start_notification_error_callback_;
 };
 
+class WebContentsDestroyingChooser : public BluetoothChooser {
+ public:
+  explicit WebContentsDestroyingChooser(base::OnceClosure on_destroy)
+      : on_destroy_(std::move(on_destroy)) {}
+  WebContentsDestroyingChooser(const WebContentsDestroyingChooser&) = delete;
+  WebContentsDestroyingChooser& operator=(const WebContentsDestroyingChooser&) =
+      delete;
+  ~WebContentsDestroyingChooser() override {
+    if (on_destroy_) {
+      std::move(on_destroy_).Run();
+    }
+  }
+
+ private:
+  base::OnceClosure on_destroy_;
+};
+
 class TestBluetoothDelegate : public BluetoothDelegate {
  public:
+  using RunBluetoothChooserCallback =
+      base::RepeatingCallback<std::unique_ptr<BluetoothChooser>(
+          RenderFrameHost*,
+          const BluetoothChooser::EventHandler&)>;
+
   TestBluetoothDelegate() = default;
   ~TestBluetoothDelegate() override = default;
   TestBluetoothDelegate(const TestBluetoothDelegate&) = delete;
@@ -425,6 +447,20 @@
     }
   }
 
+  std::unique_ptr<BluetoothChooser> RunBluetoothChooser(
+      RenderFrameHost* frame,
+      const BluetoothChooser::EventHandler& event_handler) override {
+    if (run_bluetooth_chooser_callback_) {
+      return run_bluetooth_chooser_callback_.Run(frame, event_handler);
+    }
+    return nullptr;
+  }
+
+  void set_run_bluetooth_chooser_callback(
+      RunBluetoothChooserCallback callback) {
+    run_bluetooth_chooser_callback_ = std::move(callback);
+  }
+
   std::unique_ptr<BluetoothScanningPrompt> ShowBluetoothScanningPrompt(
       RenderFrameHost* frame,
       const BluetoothScanningPrompt::EventHandler& event_handler) override {
@@ -540,6 +576,7 @@
   base::ObserverList<FramePermissionObserver> observer_list_;
   std::map<std::string, blink::WebBluetoothDeviceId> address_to_id_map_;
   std::map<blink::WebBluetoothDeviceId, std::string> id_to_address_map_;
+  RunBluetoothChooserCallback run_bluetooth_chooser_callback_;
 };
 
 class TestContentBrowserClient : public ContentBrowserClient {
@@ -936,6 +973,44 @@
   loop.RunUntilIdle();
 }
 
+TEST_F(WebBluetoothServiceImplTest, DestroyedDuringRequestDeviceReset) {
+  browser_client_.bluetooth_delegate()->set_run_bluetooth_chooser_callback(
+      base::BindLambdaForTesting(
+          [this](RenderFrameHost* frame,
+                 const BluetoothChooser::EventHandler& event_handler)
+              -> std::unique_ptr<BluetoothChooser> {
+            return std::make_unique<WebContentsDestroyingChooser>(
+                base::BindLambdaForTesting([this]() {
+                  service_ptr_ = nullptr;
+                  DeleteContents();
+                }));
+          }));
+
+  // Initial RequestDevice call opens the chooser.
+  auto options1 = blink::mojom::WebBluetoothRequestDeviceOptions::New();
+  options1->accept_all_devices = true;
+  TestFuture<WebBluetoothResult, blink::mojom::WebBluetoothDevicePtr> future1;
+  service_ptr_->RequestDevice(std::move(options1), future1.GetCallback());
+
+  // A duplicate or subsequent RequestDevice call while the chooser is open
+  // triggers device_chooser_controller_.reset(). In topologies where widget
+  // closure destroys the hosting WebContents, WebBluetoothServiceImpl is
+  // destroyed. Ensure this returns cleanly without UAF.
+  auto options2 = blink::mojom::WebBluetoothRequestDeviceOptions::New();
+  options2->accept_all_devices = true;
+  TestFuture<WebBluetoothResult, blink::mojom::WebBluetoothDevicePtr> future2;
+  service_ptr_->RequestDevice(std::move(options2), future2.GetCallback());
+
+  // The first request was cancelled during chooser closure.
+  ASSERT_TRUE(future1.IsReady());
+  EXPECT_EQ(future1.Get<0>(), WebBluetoothResult::CHOOSER_CANCELLED);
+  EXPECT_FALSE(future1.Get<1>());
+
+  // The second request bailed out due to synchronous destruction; its
+  // callback was dropped without running.
+  EXPECT_FALSE(future2.IsReady());
+}
+
 TEST_F(WebBluetoothServiceImplTest, PermissionAllowed) {
   blink::mojom::WebBluetoothLeScanFilterPtr filter = CreateScanFilter("a", "b");
   std::optional<WebBluetoothServiceImpl::ScanFilters> filters;
Loading diff…