CVE-2026-79001
Overview
Background
- GATT identifier
- a per-object string (
identifier_) that WebBluetooth assigns to each remote GATT service, characteristic, and descriptor to reference it across the Mojo boundary. - PartitionAlloc Everywhere (PA-E)
- macOS Chromium’s allocator configuration under which formatting a live object pointer with
%pyields a real 64-bit virtual heap address. - `base::UnguessableToken`
- a cryptographically random, stateless, thread-safe token used to produce unique identifiers without encoding any address or predictable state.
- `operator<<` logging
- the stream operators for the Mac GATT objects that previously logged raw object pointers (
&characteristic,service_mac, etc.).
Root Cause Analysis
On macOS, BluetoothRemoteGattServiceMac, BluetoothRemoteGattCharacteristicMac, and BluetoothRemoteGattDescriptorMac built their identifier_ with the Objective-C stringWithFormat:@"%s-%p", embedding the address of the backing CoreBluetooth object into the identifier string. Because macOS Chromium uses PA-E, %p renders an actual 64-bit browser-process heap address, and that address became part of a value handed across Mojo IPC to the renderer. A compromised renderer receiving these identifiers could recover the raw pointers and use them to infer browser heap layout, defeating ASLR. The invariant violated is that data crossing the sandbox boundary to the renderer must not encode absolute browser memory addresses.
The fix replaces the %p component with base::UnguessableToken::Create().ToString() via base::StrCat, giving each object an unguessable, address-free unique suffix, and rewrites the operator<< overloads to log GetIdentifier() instead of raw pointers so the leak does not reappear in logs.
%p; the fix substitutes a random base::UnguessableToken so identifiers stay unique without disclosing any address.Attack Path
- Compromise or control a renderer The attacker runs in (or has compromised) a renderer process that can use WebBluetooth.
- Enumerate GATT objects
Web content triggers discovery of remote GATT services, characteristics, and descriptors, causing the Mac backends to construct
identifier_strings from%p. - Receive identifiers over Mojo The identifier strings, containing embedded 64-bit heap addresses, are sent to the renderer over Mojo IPC.
- Recover heap addresses and defeat ASLR The renderer parses the pointer values out of the identifiers to map browser-process heap layout and bypass ASLR, aiding a later exploit. (inferred)
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
weak_ptr_factory_device/bluetooth/bluetooth_remote_gatt_characteristic_mac.mm |
modified | |
discovery_pending_count_device/bluetooth/bluetooth_remote_gatt_service_mac.mm |
modified |
Files Changed
device/bluetooth/bluetooth_remote_gatt_descriptor_mac.mmdevice/bluetooth/bluetooth_remote_gatt_service_mac.mmdevice/bluetooth/bluetooth_remote_gatt_characteristic_mac.mm
Audit Directions
- `%p` in cross-process stringsSearch for
stringWithFormat/sprintf/StringPrintfuses of%p(or streamed raw pointers) whose result reaches the renderer via Mojo, especially under PA-E where addresses are real. - Pointers used as identityAudit places that derive unique IDs from object addresses; replace address-derived uniqueness with
base::UnguessableTokenor opaque handles when the ID is externally visible. - `operator<<` and logging leaksReview stream operators and log statements that print
&objor object pointers, since these can surface addresses in logs and, if incorporated into IPC payloads, cross the sandbox boundary.
Patch
From ee79c0611ff70932f2672da3d580b2fe69b7b8d0 Mon Sep 17 00:00:00 2001 From: Rob Pitkin <[email protected]> Date: Mon, 13 Jul 2026 09:57:17 -0700 Subject: [PATCH] Prevent macOS WebBluetooth GATT identifiers from leaking heap pointers On macOS, BluetoothRemoteGattServiceMac, BluetoothRemoteGattCharacteristicMac, and BluetoothRemoteGattDescriptorMac constructed GATT identifiers using Objective-C format string stringWithFormat:@"%s-%p". Because macOS Chromium uses PartitionAlloc Everywhere (PA-E), formatting %p embeds 64-bit virtual heap addresses into the identifier string. When these strings are sent over Mojo IPC to the renderer process, a compromised renderer could read these pointer addresses to map browser heap layout and bypass ASLR. Replace %p with base::UnguessableToken::Create().ToString().c_str() to ensure cryptographically unguessable, stateless, thread-safe uniqueness without leaking browser heap layout. Also update operator<< logging to log GetIdentifier(). TAG=agy CONV=60ab6f7d-f6e6-4bf5-8b14-067aefab4126 Bug: 500484520 Change-Id: Ic69106765f6a13c6746601321b50a13336f67412 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8025918 Reviewed-by: Matt Reynolds <[email protected]> Commit-Queue: Rob Pitkin <[email protected]> Cr-Commit-Position: refs/heads/main@{#1661181} --- diff --git a/device/bluetooth/bluetooth_remote_gatt_characteristic_mac.mm b/device/bluetooth/bluetooth_remote_gatt_characteristic_mac.mm index b2ba785..20a2cfe2 100644 --- a/device/bluetooth/bluetooth_remote_gatt_characteristic_mac.mm +++ b/device/bluetooth/bluetooth_remote_gatt_characteristic_mac.mm @@ -9,9 +9,11 @@ #include "base/logging.h" #include "base/memory/ptr_util.h" #include "base/notimplemented.h" +#include "base/strings/strcat.h" #include "base/strings/sys_string_conversions.h" #import "base/task/single_thread_task_runner.h" #include "base/task/single_thread_task_runner.h" +#include "base/unguessable_token.h" #include "device/bluetooth/bluetooth_device_mac.h" #include "device/bluetooth/bluetooth_gatt_notify_session.h" #include "device/bluetooth/bluetooth_low_energy_adapter_apple.h" @@ -80,9 +82,8 @@ weak_ptr_factory_(this) { uuid_ = BluetoothLowEnergyAdapterApple::BluetoothUUIDWithCBUUID( [cb_characteristic_ UUID]); - identifier_ = base::SysNSStringToUTF8( - [NSString stringWithFormat:@"%s-%p", uuid_.canonical_value().c_str(), - cb_characteristic_]); + identifier_ = base::StrCat({uuid_.canonical_value(), "-", + base::UnguessableToken::Create().ToString()}); } BluetoothRemoteGattCharacteristicMac::~BluetoothRemoteGattCharacteristicMac() { @@ -483,9 +484,7 @@ static_cast<const BluetoothRemoteGattServiceMac*>( characteristic.GetService()); return out << "<BluetoothRemoteGattCharacteristicMac " - << characteristic.GetUUID().canonical_value() << "/" - << &characteristic - << ", service: " << service_mac->GetUUID().canonical_value() << "/" - << service_mac << ">"; + << characteristic.GetIdentifier() + << ", service: " << service_mac->GetIdentifier() << ">"; } } // namespace device. diff --git a/device/bluetooth/bluetooth_remote_gatt_descriptor_mac.mm b/device/bluetooth/bluetooth_remote_gatt_descriptor_mac.mm index 169dab0..dbc25d7 100644 --- a/device/bluetooth/bluetooth_remote_gatt_descriptor_mac.mm +++ b/device/bluetooth/bluetooth_remote_gatt_descriptor_mac.mm @@ -7,9 +7,11 @@ #import "base/apple/foundation_util.h" #include "base/functional/bind.h" #include "base/notimplemented.h" +#include "base/strings/strcat.h" #include "base/strings/sys_string_conversions.h" #import "base/task/single_thread_task_runner.h" #include "base/task/single_thread_task_runner.h" +#include "base/unguessable_token.h" #include "device/bluetooth/bluetooth_low_energy_adapter_apple.h" #import "device/bluetooth/bluetooth_remote_gatt_characteristic_mac.h" @@ -50,9 +52,8 @@ : gatt_characteristic_(characteristic), cb_descriptor_(descriptor) { uuid_ = BluetoothLowEnergyAdapterApple::BluetoothUUIDWithCBUUID( [cb_descriptor_ UUID]); - identifier_ = base::SysNSStringToUTF8( - [NSString stringWithFormat:@"%s-%p", uuid_.canonical_value().c_str(), - cb_descriptor_]); + identifier_ = base::StrCat({uuid_.canonical_value(), "-", + base::UnguessableToken::Create().ToString()}); } std::string BluetoothRemoteGattDescriptorMac::GetIdentifier() const { @@ -188,10 +189,9 @@ static_cast<const BluetoothRemoteGattCharacteristicMac*>( descriptor.GetCharacteristic()); return out << "<BluetoothRemoteGattDescriptorMac " - << descriptor.GetUUID().canonical_value() << "/" << &descriptor - << ", characteristic: " - << characteristic_mac->GetUUID().canonical_value() << "/" - << characteristic_mac << ">"; + << descriptor.GetIdentifier() + << ", characteristic: " << characteristic_mac->GetIdentifier() + << ">"; } } // namespace device. diff --git a/device/bluetooth/bluetooth_remote_gatt_service_mac.mm b/device/bluetooth/bluetooth_remote_gatt_service_mac.mm index 1a51be0..81900c1 100644 --- a/device/bluetooth/bluetooth_remote_gatt_service_mac.mm +++ b/device/bluetooth/bluetooth_remote_gatt_service_mac.mm @@ -12,7 +12,9 @@ #include "base/logging.h" #include "base/memory/ptr_util.h" #include "base/notimplemented.h" +#include "base/strings/strcat.h" #include "base/strings/sys_string_conversions.h" +#include "base/unguessable_token.h" #include "device/bluetooth/bluetooth_low_energy_adapter_apple.h" #include "device/bluetooth/bluetooth_low_energy_device_mac.h" #include "device/bluetooth/bluetooth_remote_gatt_characteristic_mac.h" @@ -30,8 +32,8 @@ discovery_pending_count_(0) { uuid_ = BluetoothLowEnergyAdapterApple::BluetoothUUIDWithCBUUID([service_ UUID]); - identifier_ = base::SysNSStringToUTF8([NSString - stringWithFormat:@"%s-%p", uuid_.canonical_value().c_str(), service_]); + identifier_ = base::StrCat({uuid_.canonical_value(), "-", + base::UnguessableToken::Create().ToString()}); } BluetoothRemoteGattServiceMac::~BluetoothRemoteGattServiceMac() {} @@ -195,10 +197,8 @@ const BluetoothRemoteGattServiceMac& service) { const BluetoothLowEnergyDeviceMac* bluetooth_device_mac_ = static_cast<const BluetoothLowEnergyDeviceMac*>(service.GetDevice()); - return out << "<BluetoothRemoteGattServiceMac " - << service.GetUUID().canonical_value() << "/" << &service - << ", device: " << bluetooth_device_mac_->GetAddress() << "/" - << bluetooth_device_mac_ << ">"; + return out << "<BluetoothRemoteGattServiceMac " << service.GetIdentifier() + << ", device: " << bluetooth_device_mac_->GetAddress() << ">"; } } // namespace device