Critical watch CVSS 7.5 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
7.5
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.
Tracker517418936
Fix commit4bb7a1ea296c (chromium/src) +2/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-28 (per vendor advisory)
Fix landed2026-06-02 (public git)
Disclosed2026-06-08
Patch gap6 days — fix public in source before the advisory

Background

`BluetoothGattDiscovererWinrt`
The WinRT-backed helper in device/bluetooth/bluetooth_gatt_discoverer_winrt.cc that walks a device’s GATT services and characteristics to build up its attribute list.
`callback_`
A one-shot base::OnceCallback that reports discovery success or failure exactly once and is consumed via std::move(callback_).Run(...).
`RunCallbackIfDone`
The method invoked after the service/characteristic iteration loop completes that runs callback_ a second time once all pending async results have returned.
Use-after-free (UAF)
A memory-safety bug in which code dereferences or acts on an object after it has been destroyed, here triggered by invoking an already-consumed callback that tears down the discoverer.

Root Cause Analysis

While iterating over the GATT service and characteristic lists, OnGetGattServices and OnGetCharacteristics issue per-element WinRT calls such as GattDeviceService::OpenAsync() and base::win::PostAsyncResults. When one of these calls returns a failure hr, the code logs the error and reports failure by consuming the one-shot callback with std::move(callback_).Run(false), but it did not stop the loop afterward. Because control fell through, the loop continued and later reached RunCallbackIfDone, which invoked the already-moved callback_ a second time. Running a consumed base::OnceCallback — and running the completion path twice — violates the invariant that the discovery callback fires exactly once; the failure callback typically destroys or advances the owning discovery state, so the second invocation operates on freed state.

The fix adds an early return immediately after the failure std::move(callback_).Run(false) in both methods, so the callback is consumed and dispatched exactly once and no stale second dispatch occurs.

Key insight
The core mistake is that the error branches consumed the one-shot callback_ but let the enclosing loop continue into RunCallbackIfDone, double-invoking a moved-from callback; adding return after std::move(callback_).Run(false) guarantees the callback fires only once.

Attack Path

  1. Start GATT discovery An attacker-controlled or malicious Bluetooth peripheral is connected and the browser begins GATT service/characteristic discovery via BluetoothGattDiscovererWinrt.
  2. Force an async failure mid-iteration During the loop in OnGetGattServices or OnGetCharacteristics, one WinRT call such as OpenAsync() or PostAsyncResults is induced to return a failure hr while further list elements remain.
  3. First callback consumes state The failure branch runs std::move(callback_).Run(false), reporting failure and tearing down or advancing the discovery state that owns the discoverer.
  4. Fall-through to second invocation Without the missing return, control continues out of the loop into RunCallbackIfDone, which invokes the now moved-from callback_ again against freed state, producing the use-after-free.

Impact Assessment

An attacker who can drive Bluetooth GATT discovery to fail mid-iteration gains a use-after-free in the browser process on Windows (the WinRT backend), which can lead to memory corruption and potentially exploitable control over freed objects. Preconditions include a Bluetooth-capable Windows target performing service or characteristic discovery against a device whose WinRT calls can be made to fail partway through the list.

Files Changed

  • device/bluetooth/bluetooth_gatt_discoverer_winrt.cc

Audit Directions

  • Consumed one-shot callback without return
    Search loops that call std::move(callback_).Run(...) on error but continue iterating or fall through to a shared completion helper like RunCallbackIfDone.
  • Deferred completion after early failure
    Audit async iteration patterns where a per-element failure reports the result immediately yet a post-loop “if done” aggregator can also fire the same callback.
  • Missing early exit in error branches
    Review WinRT and other async backends for error paths that log and run a callback but omit the terminating return, breaking the exactly-once invariant.
From 4bb7a1ea296c757b7ce5a0ae7bedfc4226f15df2 Mon Sep 17 00:00:00 2001
From: Matt Reynolds <mattreynolds@google.com>
Date: Mon, 01 Jun 2026 19:23:34 -0700
Subject: [PATCH] bluetooth: Avoid double invoking callback after failed winrt call

In OnGetGattServices and OnGetCharacteristics, if an API call fails while
iterating over a list then the callback is invoked twice: once to report
the failure and then again when RunCallbackIfDone is called after the
loop exits. This CL ensures these methods return after invoking the
callback the first time.

Bug: 517418936
Change-Id: I6ed22b9a7d2aede6c807054b88f427d8564cf649
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7883805
Commit-Queue: Matt Reynolds <mattreynolds@chromium.org>
Reviewed-by: Alvin Ji <alvinji@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1639876}
---

diff --git a/device/bluetooth/bluetooth_gatt_discoverer_winrt.cc b/device/bluetooth/bluetooth_gatt_discoverer_winrt.cc
index ae77850..5f2c354 100644
--- a/device/bluetooth/bluetooth_gatt_discoverer_winrt.cc
+++ b/device/bluetooth/bluetooth_gatt_discoverer_winrt.cc
@@ -302,6 +302,7 @@
       BLUETOOTH_LOG(DEBUG) << "GattDeviceService::OpenAsync() failed: "
                            << logging::SystemErrorCodeToString(hr);
       std::move(callback_).Run(false);
+      return;
     }
 
     hr = base::win::PostAsyncResults(
@@ -423,6 +424,7 @@
       BLUETOOTH_LOG(DEBUG) << "PostAsyncResults failed: "
                            << logging::SystemErrorCodeToString(hr);
       std::move(callback_).Run(false);
+      return;
     }
   }
 
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free in BluetoothGattDiscovererWinrt::OnGetCharacteristics

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Use-After-Free vulnerability exists in the Windows Web Bluetooth implementation when handling descriptor retrieval failures. If base::win::PostAsyncResults fails during discovery, it synchronously executes a callback that destroys the discoverer object, yet the function continues to loop over freed member variables. This can lead to a browser-process crash or potential control flow hijack.

Affected files:

  • device/bluetooth/bluetooth_gatt_discoverer_winrt.cc
  • device/bluetooth/bluetooth_gatt_discoverer_winrt.h
  • device/bluetooth/bluetooth_device_winrt.cc

Estimated timestamp from git blame: 2018-08-06

Detailed Description

A potential Use-After-Free (UAF) vulnerability has been identified in BluetoothGattDiscovererWinrt::OnGetCharacteristics in device/bluetooth/bluetooth_gatt_discoverer_winrt.cc.

In OnGetCharacteristics, the code obtains a reference to a nested vector of characteristics stored inside the member service_to_characteristics_map_:

// device/bluetooth/bluetooth_gatt_discoverer_winrt.cc
378:   auto& characteristics_list =
379:       service_to_characteristics_map_[service_attribute_handle];

It then iterates over this list using a range-based for loop to retrieve descriptors for each characteristic:

386:   for (const auto& gatt_characteristic : characteristics_list) {
...
416:     hr = base::win::PostAsyncResults(
417:         std::move(get_descriptors_op),
418:         base::BindOnce(&BluetoothGattDiscovererWinrt::OnGetDescriptors,
419:                        weak_ptr_factory_.GetWeakPtr(),
420:                        characteristic_attribute_handle));
421: 
422:     if (FAILED(hr)) {
423:       BLUETOOTH_LOG(DEBUG) << "PostAsyncResults failed: "
424:                            << logging::SystemErrorCodeToString(hr);
425:       std::move(callback_).Run(false);
426:     }
427:   }

If base::win::PostAsyncResults fails (returning a failure HRESULT due to an unexpected disconnect or internal COM state error), the code invokes std::move(callback_).Run(false). Running this callback triggers a synchronous sequence of events that resolves to BluetoothDeviceWinrt::OnGattDiscoveryComplete, which deletes the owning BluetoothGattDiscovererWinrt instance via gatt_discoverer_.reset() (at device/bluetooth/bluetooth_device_winrt.cc:800).

Because there is no return or break statement after line 425, the range-based for loop continues to the next iteration. However, the destruction of the discoverer object has already deallocated the parent map (service_to_characteristics_map_), freeing the backing memory of the characteristics_list vector. Consequently, the loop’s compiler-generated iterators are left dangling, resulting in a Use-After-Free when they are incremented/dereferenced in subsequent iterations.

Note that our analysis is based on code inspection and static tracing; our automated tools have not run code or verified a dynamic proof-of-concept.

Potential Attack Steps / Triggering Conditions

An attacker trying to trigger this issue would potentially follow these steps:

  1. Persuade a user to connect to a custom, malicious Bluetooth LE peripheral via the Web Bluetooth chooser interface.
  2. During GATT discovery, when the browser attempts to retrieve descriptors for the characteristics of a service, the peripheral can abort or disconnect the connection or trigger an RPC failure.
  3. This connection drop causes GetDescriptorsAsync or the subsequent PostAsyncResults call to fail (FAILED(hr) resolves to true).
  4. The callback runs synchronously, destroying the discoverer, while the loop continues iterating on the freed heap memory.

Suggested Remediation

To resolve this issue, add a return; statement immediately after invoking the callback inside the FAILED(hr) block to stop further execution of the loop and safely exit the method:

    if (FAILED(hr)) {
      BLUETOOTH_LOG(DEBUG) << "PostAsyncResults failed: "
                           << logging::SystemErrorCodeToString(hr);
      std::move(callback_).Run(false);
      return;
    }

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker