← Chrome — 2026-W39

e75c6ae03b77ce35ac369222cbfad68da9cc4e40  [M152] bluetooth: Fix reentrancy UAF during ShowPermissionPrompt

severity medium class UAF CVSS 8.3 signal score 8.79 content/browser/ AI review CONFIRMED
Structural signals: null_guard (+2.59), uaf_lifetime (+2.2), msg_strong (+3.0), area:content/browser/ (+1.0)

The bug at a glance

Browser-process heap use-after-free write: per the commit message, assigning prompt_ after ShowBluetoothScanningPrompt() returns writes into a BluetoothDeviceScanningPromptController already destroyed by a nested message loop. Rated high rather than critical because it requires navigator.bluetooth.requestLEScan() (an experimental Web Bluetooth scanning API, typically flag-gated in stock builds (inferred)), a fullscreen state to drop, and a timed frame detach.

A renderer-triggerable browser-process UAF is a sandbox-escape candidate: the attacker races an iframe removal against the fullscreen-exit nested run loop spun inside the prompt display. Defenders care about the general pattern of synchronous embedder UI calls that re-enter the message loop and free their caller.

Root cause

BluetoothDeviceScanningPromptController::ShowPermissionPrompt synchronously called delegate->ShowBluetoothScanningPrompt() and stored the return value into the member prompt_. On desktop the prompt drops fullscreen, which can spin a nested message loop that processes a pending FrameDetach IPC, destroying the requesting subframe’s RenderFrameHostImpl and the controller it owns; the subsequent prompt_ = … write hits freed memory. The fix takes a WeakPtr before the call and bails if the controller died or the event was already handled synchronously, cancels if no prompt was created, and makes AdvertisementClient callbacks default-invoke (PROMPT_CANCELED / WATCH_ADVERTISEMENTS_ABORTED) and null-checked so dropped callbacks don’t hang or double-run.

Key code

WeakPtr liveness check after reentrant prompt display

+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
+  auto prompt = delegate->ShowBluetoothScanningPrompt(
+      &*render_frame_host_, std::move(prompt_event_handler));
+  if (!weak_this || prompt_event_received_) {
+    return;
+  }
-    prompt_ = delegate->ShowBluetoothScanningPrompt(
-        &*render_frame_host_, std::move(prompt_event_handler));

Patch walkthrough

  • content/browser/bluetooth/bluetooth_device_scanning_prompt_controller.cc — Core fix: captures weak_this before ShowBluetoothScanningPrompt(), returns if the controller was destroyed or the event already fired during the call, and cancels immediately on a null prompt instead of assigning into a possibly freed object.
  • content/browser/bluetooth/advertisement_client.cc — Wraps the request callbacks with mojo::WrapCallbackWithDefaultInvokeIfNotRun so a client torn down mid-request still replies (PROMPT_CANCELED / WATCH_ADVERTISEMENTS_ABORTED) rather than dropping a Mojo callback.
  • content/browser/bluetooth/advertisement_client.h — RunCallback now checks callback_ before running it, avoiding running an already-consumed OnceCallback on reentrant paths.
  • content/browser/bluetooth/web_bluetooth_service_impl_browsertest.cc — Adds FrameDetachDuringShowBluetoothScanningPrompt, which removes the requesting iframe synchronously inside ShowBluetoothScanningPrompt and expects a ‘RenderFrame deleted’ error instead of a crash.

Background

navigator.bluetooth.requestLEScan() — Web Bluetooth scanning API that asks the browser to show a scanning permission prompt before delivering BLE advertisements.

Nested message loop — A run loop spun inside a synchronous call that can dispatch unrelated tasks/IPCs, letting objects higher on the stack be destroyed (reentrancy).

BluetoothDeviceScanningPromptController — Browser-side object owned via the frame’s Web Bluetooth service that shows the scanning prompt and routes its result.

Vulnerability window

  1. Reachability — Web content in a subframe calling requestLEScan(); the scanning API is experimental and likely flag-gated in stock builds (inferred), and the fullscreen-drop nested loop requires the tab to be fullscreen (user gesture) on desktop.
  2. Trigger — The parent removes the requesting iframe so its FrameDetach IPC is processed inside the nested loop spun while the prompt drops fullscreen.
  3. Primitive — Heap UAF write of the prompt unique_ptr into the freed BluetoothDeviceScanningPromptController in the browser process.
  4. Escalation — Reclaiming the freed allocation could turn the write into browser-process memory corruption and a sandbox escape (inferred).

Proof of concept

Conceptually: a fullscreen page embeds an iframe that calls requestLEScan(); concurrently the parent removes that iframe so the detach is handled during the nested message loop entered while the scanning prompt exits fullscreen, freeing the controller before prompt_ is assigned. The added browsertest models this by removing the iframe from inside ShowBluetoothScanningPrompt.

Exploitation

  1. Setup — Get the page fullscreen and an iframe positioned to request LE scanning (inferred ordering).
  2. Race/reentrancy — Queue the iframe-removal IPC so it lands inside the fullscreen-exit nested loop (inferred).
  3. Corruption — The freed controller’s prompt_ slot is written after return; heap grooming in the browser process would be needed to make this useful (inferred).

Detection & hunting

For defenders and detection engineers:

  • weggli/grep: member assignment from a delegate->ShowPrompt(…) / ShowChooser(…) return value without a preceding GetWeakPtr() — Find browser code that stores a synchronously returned UI object into a member after an embedder call that may spin a run loop.
  • ASan heap-use-after-free in BluetoothDeviceScanningPromptController::ShowPermissionPrompt — Crash triage signal on unpatched builds; check for this frame in fuzzer/ClusterFuzz reports.

Audit directions

  • content/browser/bluetooth/ (BluetoothDeviceChooserController::ShowChooser and similar) — Check other chooser/prompt paths for the same assign-after-synchronous-UI pattern with no liveness check.
  • Other device/permission prompts that exit fullscreen (USB, HID, Serial choosers, permission bubbles) — Any prompt that drops fullscreen can re-enter the loop; verify callers guard with WeakPtr before touching members.
  • Mojo callbacks stored by per-frame service objects — Ensure stored RequestCallbacks are default-invoked or null-checked when the owner is torn down mid-request.

Before / after

Loading diff…