Chrome · Bluetooth › Bluetooth
CVE-2026-113310
OOB in Bluetooth (Bluetooth)
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdevice/bluetooth/bluez/bluetooth_adapter_bluez.cc |
modified | |
ifdevice/bluetooth/bluez/bluetooth_pairing_bluez.cc |
modified | |
TEST_Pdevice/bluetooth/bluez/bluetooth_bluez_unittest.cc |
modified | |
fordevice/bluetooth/bluez/bluetooth_bluez_unittest.cc |
modified |
Files Changed
device/bluetooth/bluez/bluetooth_pairing_bluez.ccdevice/bluetooth/bluez/bluetooth_pairing_bluez.hdevice/bluetooth/bluez/bluetooth_adapter_bluez.ccdevice/bluetooth/bluez/bluetooth_bluez_unittest.cc
Patch
From 8b38650ae0b4930bc1cfc172ebe88c7d3d4a1b9e Mon Sep 17 00:00:00 2001 From: Rob Pitkin <[email protected]> Date: Mon, 12 Oct 2026 22:04:50 -0700 Subject: [PATCH] bluetooth: Fix UAF in BluetoothPairingBlueZ on synchronous cancel PairingDelegate implementations (such as DevicePairingHandler in Ash when a system-modal pairing dialog fails to open or is immediately dismissed) can synchronously invoke BluetoothDevice::CancelPairing() or BluetoothAdapter::RemovePairingDelegate() from inside delegate callbacks like DisplayPinCode(), DisplayPasskey(), or KeysEntered(). Because BluetoothDeviceBlueZ::CancelPairing() synchronously calls EndPairing(), which resets std::unique_ptr<BluetoothPairingBlueZ>, the active BluetoothPairingBlueZ instance is destroyed while its method or caller is still on the stack. This causes a use-after-free when: 1. BluetoothPairingBlueZ::DisplayPinCode() or KeysEntered() accesses `device_` after `pairing_delegate_` returns. 2. BluetoothAdapterBlueZ::DisplayPasskey() calls `pairing->KeysEntered(entered)` after `pairing->DisplayPasskey()` synchronously destroyed `pairing`. This CL adds a WeakPtrFactory to BluetoothPairingBlueZ and checks weak pointer validity after external delegate and callback invocations before accessing member state or making follow-up calls. Bug: 566687517 Change-Id: I4d16dff447062be9cae39cfd65ef8433ef87e0e9 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8477932 Reviewed-by: Matt Reynolds <[email protected]> Commit-Queue: Rob Pitkin <[email protected]> Reviewed-by: Vincent Scheib <[email protected]> Cr-Commit-Position: refs/heads/main@{#1717336} --- diff --git a/device/bluetooth/bluez/bluetooth_adapter_bluez.cc b/device/bluetooth/bluez/bluetooth_adapter_bluez.cc index 71039738..702348f 100644 --- a/device/bluetooth/bluez/bluetooth_adapter_bluez.cc +++ b/device/bluetooth/bluez/bluetooth_adapter_bluez.cc @@ -1076,9 +1076,15 @@ if (!pairing) return; - if (entered == 0) + if (entered == 0) { + // DisplayPasskey() notifies the pairing delegate, which may cancel pairing + // and destroy `pairing` before returning. + base::WeakPtr<BluetoothPairingBlueZ> weak_pairing = pairing->GetWeakPtr(); pairing->DisplayPasskey(passkey); - + if (!weak_pairing) { + return; + } + } pairing->KeysEntered(entered); } diff --git a/device/bluetooth/bluez/bluetooth_pairing_bluez.cc b/device/bluetooth/bluez/bluetooth_pairing_bluez.cc index c42d36e..bb2bba56 100644 --- a/device/bluetooth/bluez/bluetooth_pairing_bluez.cc +++ b/device/bluetooth/bluez/bluetooth_pairing_bluez.cc @@ -65,7 +65,13 @@ void BluetoothPairingBlueZ::DisplayPinCode(const std::string& pincode) { ResetCallbacks(); pairing_delegate_used_ = true; + + base::WeakPtr<BluetoothPairingBlueZ> weak_this = + weak_ptr_factory_.GetWeakPtr(); pairing_delegate_->DisplayPinCode(device_, pincode); + if (!weak_this) { + return; + } // If this is not an outgoing connection to the device, the pairing context // needs to be cleaned up again as there's no reliable indication of @@ -108,7 +114,13 @@ void BluetoothPairingBlueZ::KeysEntered(uint16_t entered) { pairing_delegate_used_ = true; + + base::WeakPtr<BluetoothPairingBlueZ> weak_this = + weak_ptr_factory_.GetWeakPtr(); pairing_delegate_->KeysEntered(device_, entered); + if (!weak_this) { + return; + } // If this is not an outgoing connection to the device, the pairing context // needs to be cleaned up again as there's no reliable indication of @@ -169,6 +181,10 @@ return pairing_delegate_; } +base::WeakPtr<BluetoothPairingBlueZ> BluetoothPairingBlueZ::GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + void BluetoothPairingBlueZ::ResetCallbacks() { if (!pincode_callback_.is_null()) { std::move(pincode_callback_) diff --git a/device/bluetooth/bluez/bluetooth_pairing_bluez.h b/device/bluetooth/bluez/bluetooth_pairing_bluez.h index e6843b21..aa5221df 100644 --- a/device/bluetooth/bluez/bluetooth_pairing_bluez.h +++ b/device/bluetooth/bluez/bluetooth_pairing_bluez.h @@ -8,6 +8,7 @@ #include <stdint.h> #include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" #include "device/bluetooth/bluetooth_device.h" #include "device/bluetooth/dbus/bluetooth_agent_service_provider.h" @@ -114,6 +115,8 @@ // Returns the pairing delegate being used by this pairing object. device::BluetoothDevice::PairingDelegate* GetPairingDelegate() const; + base::WeakPtr<BluetoothPairingBlueZ> GetWeakPtr(); + private: // Internal method to run the current set of callbacks with canceled state // because a new request has arrived that supersedes them. @@ -146,6 +149,8 @@ passkey_callback_; bluez::BluetoothAgentServiceProvider::Delegate::ConfirmationCallback confirmation_callback_; + + base::WeakPtrFactory<BluetoothPairingBlueZ> weak_ptr_factory_{this}; }; } // namespace bluez diff --git a/device/bluetooth/bluez/bluetooth_bluez_unittest.cc b/device/bluetooth/bluez/bluetooth_bluez_unittest.cc index 899a93aa..3b681ca 100644 --- a/device/bluetooth/bluez/bluetooth_bluez_unittest.cc +++ b/device/bluetooth/bluez/bluetooth_bluez_unittest.cc @@ -3612,6 +3612,101 @@ EXPECT_FALSE(device->IsPaired()); } +TEST_P(BluetoothBlueZTestP, PairingCancelledSynchronouslyAtDisplayPinCode) { + fake_bluetooth_device_client_->SetSimulationIntervalMs(10); + + GetAdapter(); + DiscoverDevices(); + + BluetoothDevice* device = adapter_->GetDevice( + bluez::FakeBluetoothDeviceClient::kDisplayPinCodeAddress); + ASSERT_TRUE(device != nullptr); + ASSERT_FALSE(device->IsPaired()); + + StrictMock<MockPairingDelegate> pairing_delegate; + // Note: Synchronous cancel + // The delegate cancels pairing from inside its callout. CancelPairing() + // synchronously ends pairing, destroying the BluetoothPairingBlueZ while + // the method that called the delegate is still on the stack. That method + // must check its WeakPtr before touching members, or it reads freed memory. + EXPECT_CALL(pairing_delegate, DisplayPinCode(_, "123456")) + .WillOnce([](BluetoothDevice* device, const std::string& pincode) { + device->CancelPairing(); + }); + + base::test::TestFuture<std::optional<BluetoothDevice::ConnectErrorCode>> + connect_future; + PerformConnect(device, &pairing_delegate, connect_future.GetCallback()); + EXPECT_EQ(BluetoothDevice::ERROR_AUTH_CANCELED, connect_future.Get()); + EXPECT_FALSE(device->IsConnected()); + EXPECT_FALSE(device->IsConnecting()); + EXPECT_FALSE(device->IsPaired()); +} + +TEST_P(BluetoothBlueZTestP, PairingCancelledSynchronouslyAtDisplayPasskey) { + fake_bluetooth_device_client_->SetSimulationIntervalMs(10); + + GetAdapter(); + DiscoverDevices(); + + BluetoothDevice* device = adapter_->GetDevice( + bluez::FakeBluetoothDeviceClient::kDisplayPasskeyAddress); + ASSERT_TRUE(device != nullptr); + ASSERT_FALSE(device->IsPaired()); + + StrictMock<MockPairingDelegate> pairing_delegate; + // See "Note: Synchronous cancel" above. Cancelling inside DisplayPasskey() + // means BluetoothAdapterBlueZ::DisplayPasskey() must not go on to call + // KeysEntered() on the destroyed pairing. + EXPECT_CALL(pairing_delegate, DisplayPasskey(_, 123456U)) + .WillOnce([](BluetoothDevice* device, uint32_t passkey) { + device->CancelPairing(); + }); + EXPECT_CALL(pairing_delegate, KeysEntered(_, _)).Times(0); + + base::test::TestFuture<std::optional<BluetoothDevice::ConnectErrorCode>> + connect_future; + PerformConnect(device, &pairing_delegate, connect_future.GetCallback()); + EXPECT_EQ(BluetoothDevice::ERROR_AUTH_CANCELED, connect_future.Get()); + EXPECT_FALSE(device->IsConnected()); + EXPECT_FALSE(device->IsConnecting()); + EXPECT_FALSE(device->IsPaired()); +} + +TEST_P(BluetoothBlueZTestP, PairingCancelledSynchronouslyAtKeysEntered) { + fake_bluetooth_device_client_->SetSimulationIntervalMs(10); + + GetAdapter(); + DiscoverDevices(); + + BluetoothDevice* device = adapter_->GetDevice( + bluez::FakeBluetoothDeviceClient::kDisplayPasskeyAddress); + ASSERT_TRUE(device != nullptr); + ASSERT_FALSE(device->IsPaired()); + + StrictMock<MockPairingDelegate> pairing_delegate; + EXPECT_CALL(pairing_delegate, DisplayPasskey(_, 123456U)).Times(1); + // See "Note: Synchronous cancel" above. Cancelling inside the final + // KeysEntered() means BluetoothPairingBlueZ::KeysEntered() must not read + // `device_` afterwards. + testing::InSequence seq; + for (int i = 0; i < 7; ++i) { + EXPECT_CALL(pairing_delegate, KeysEntered(_, i)); + } + EXPECT_CALL(pairing_delegate, KeysEntered(_, 7)) + .WillOnce([](BluetoothDevice* device, uint32_t entered) { + device->CancelPairing(); + }); + + base::test::TestFuture<std::optional<BluetoothDevice::ConnectErrorCode>> + connect_future; + PerformConnect(device, &pairing_delegate, connect_future.GetCallback()); + EXPECT_EQ(BluetoothDevice::ERROR_AUTH_CANCELED, connect_future.Get()); + EXPECT_FALSE(device->IsConnected()); + EXPECT_FALSE(device->IsConnecting()); + EXPECT_FALSE(device->IsPaired()); +} + TEST_P(BluetoothBlueZTestP, PairingCancelledInFlight) { fake_bluetooth_device_client_->SetSimulationIntervalMs(10);
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/device/bluetooth/bluez/bluetooth_bluez_unittest.cc b/device/bluetooth/bluez/bluetooth_bluez_unittest.cc
index 899a93aa..3b681ca 100644
--- a/device/bluetooth/bluez/bluetooth_bluez_unittest.cc
+++ b/device/bluetooth/bluez/bluetooth_bluez_unittest.cc
@@ -3612,6 +3612,101 @@
EXPECT_FALSE(device->IsPaired());
}
+TEST_P(BluetoothBlueZTestP, PairingCancelledSynchronouslyAtDisplayPinCode) {
+ fake_bluetooth_device_client_->SetSimulationIntervalMs(10);
+
+ GetAdapter();
+ DiscoverDevices();
+
+ BluetoothDevice* device = adapter_->GetDevice(
+ bluez::FakeBluetoothDeviceClient::kDisplayPinCodeAddress);
+ ASSERT_TRUE(device != nullptr);
+ ASSERT_FALSE(device->IsPaired());
+
+ StrictMock<MockPairingDelegate> pairing_delegate;
+ // Note: Synchronous cancel
+ // The delegate cancels pairing from inside its callout. CancelPairing()
+ // synchronously ends pairing, destroying the BluetoothPairingBlueZ while
+ // the method that called the delegate is still on the stack. That method
+ // must check its WeakPtr before touching members, or it reads freed memory.
+ EXPECT_CALL(pairing_delegate, DisplayPinCode(_, "123456"))
+ .WillOnce([](BluetoothDevice* device, const std::string& pincode) {
+ device->CancelPairing();
+ });
+
+ base::test::TestFuture<std::optional<BluetoothDevice::ConnectErrorCode>>
+ connect_future;
+ PerformConnect(device, &pairing_delegate, connect_future.GetCallback());
+ EXPECT_EQ(BluetoothDevice::ERROR_AUTH_CANCELED, connect_future.Get());
+ EXPECT_FALSE(device->IsConnected());
+ EXPECT_FALSE(device->IsConnecting());
+ EXPECT_FALSE(device->IsPaired());
+}
+
+TEST_P(BluetoothBlueZTestP, PairingCancelledSynchronouslyAtDisplayPasskey) {
+ fake_bluetooth_device_client_->SetSimulationIntervalMs(10);
+
+ GetAdapter();
+ DiscoverDevices();
+
+ BluetoothDevice* device = adapter_->GetDevice(
+ bluez::FakeBluetoothDeviceClient::kDisplayPasskeyAddress);
+ ASSERT_TRUE(device != nullptr);
+ ASSERT_FALSE(device->IsPaired());
+
+ StrictMock<MockPairingDelegate> pairing_delegate;
+ // See "Note: Synchronous cancel" above. Cancelling inside DisplayPasskey()
+ // means BluetoothAdapterBlueZ::DisplayPasskey() must not go on to call
+ // KeysEntered() on the destroyed pairing.
+ EXPECT_CALL(pairing_delegate, DisplayPasskey(_, 123456U))
+ .WillOnce([](BluetoothDevice* device, uint32_t passkey) {
+ device->CancelPairing();
+ });
+ EXPECT_CALL(pairing_delegate, KeysEntered(_, _)).Times(0);
+
+ base::test::TestFuture<std::optional<BluetoothDevice::ConnectErrorCode>>
+ connect_future;
+ PerformConnect(device, &pairing_delegate, connect_future.GetCallback());
+ EXPECT_EQ(BluetoothDevice::ERROR_AUTH_CANCELED, connect_future.Get());
+ EXPECT_FALSE(device->IsConnected());
+ EXPECT_FALSE(device->IsConnecting());
+ EXPECT_FALSE(device->IsPaired());
+}
+
+TEST_P(BluetoothBlueZTestP, PairingCancelledSynchronouslyAtKeysEntered) {
+ fake_bluetooth_device_client_->SetSimulationIntervalMs(10);
+
+ GetAdapter();
+ DiscoverDevices();
+
+ BluetoothDevice* device = adapter_->GetDevice(
+ bluez::FakeBluetoothDeviceClient::kDisplayPasskeyAddress);
+ ASSERT_TRUE(device != nullptr);
+ ASSERT_FALSE(device->IsPaired());
+
+ StrictMock<MockPairingDelegate> pairing_delegate;
+ EXPECT_CALL(pairing_delegate, DisplayPasskey(_, 123456U)).Times(1);
+ // See "Note: Synchronous cancel" above. Cancelling inside the final
+ // KeysEntered() means BluetoothPairingBlueZ::KeysEntered() must not read
+ // `device_` afterwards.
+ testing::InSequence seq;
+ for (int i = 0; i < 7; ++i) {
+ EXPECT_CALL(pairing_delegate, KeysEntered(_, i));
+ }
+ EXPECT_CALL(pairing_delegate, KeysEntered(_, 7))
+ .WillOnce([](BluetoothDevice* device, uint32_t entered) {
+ device->CancelPairing();
+ });
+
+ base::test::TestFuture<std::optional<BluetoothDevice::ConnectErrorCode>>
+ connect_future;
+ PerformConnect(device, &pairing_delegate, connect_future.GetCallback());
+ EXPECT_EQ(BluetoothDevice::ERROR_AUTH_CANCELED, connect_future.Get());
+ EXPECT_FALSE(device->IsConnected());
+ EXPECT_FALSE(device->IsConnecting());
+ EXPECT_FALSE(device->IsPaired());
+}
+
TEST_P(BluetoothBlueZTestP, PairingCancelledInFlight) {
fake_bluetooth_device_client_->SetSimulationIntervalMs(10);
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.
References
On This Page