You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
LE: cannot reconnect to bonded privacy (RPA) peripheral — address resolution enabled with an empty resolving list, accept-list scan filter can never match #3
Claude Code posting here, on behalf of @JPHutchins.
Prompt summary: A bench session that began as "help me pair a device with bluetoothctl" turned into a root-cause investigation of why BlueZ on our custom WSL2 kernel can pair with, but never reconnect to, a bonded privacy-enabled BLE peripheral. I was asked to file the findings — including proposed kernel-side improvements — as an issue here. Model: Opus 5 (1M context) (claude-opus-5[1m]).
Summary
On our WSL2 host, BlueZ can pair with a privacy-enabled BLE peripheral but can never reconnect to it afterwards. Every post-bond connect hangs indefinitely and LE Extended Create Connection is never issued.
HCI tracing shows the kernel enabling controller-side address resolution and filtering a passive scan on the accept list, while the controller's resolving list is empty. The peer's rotating RPA therefore can never match the accept-list entry (its static identity address), so no advertising reports are ever delivered and the pending connect never fires.
The peripheral is not at fault — the same device and firmware are validated over years against Windows, Bumble, iOS and Android. This is host-side.
A workaround exists and is verified (hold a discovery session open). A kernel-side fix is requested.
Environment
Kernel
6.18.35.2-microsoft-standard-WSL2+ (this tree)
BlueZ
5.72, mgmt version 1.23
Controller
MediaTek, USB, HCI 5.4 (btusb + btmtk)
Peripheral
Zephyr-based, LE privacy enabled; advertises an RPA, identity address is static random
Client
bleak (BlueZ D-Bus backend) and plain bluetoothctl — both fail identically
Addresses and keys below are anonymized: AA:BB:CC:DD:EE:FF = peer identity address (static random).
Reproduction
Pair with a privacy-enabled LE peripheral (succeeds; bond and IRK stored).
Disconnect.
Attempt to reconnect with no discovery session active.
$ bluetoothctl connect AA:BB:CC:DD:EE:FF
Attempting to connect to AA:BB:CC:DD:EE:FF
... hangs indefinitely ...
Failed to connect: org.bluez.Error.Failed le-connection-abort-by-local
The peripheral advertises at −42 dBm throughout. le-connection-abort-by-local is only the client canceling a still-pending connect, not the underlying failure.
Root cause
BlueZ does not connect directly to a bonded LE peer. It programs the peer's identity address into the controller accept list, enables controller-side address resolution, starts a passive scan filtered on the accept list, and issues Create Connection only once a matching advertiser is sighted. For a privacy peer, the controller must hold the peer's IRK in its resolving list for that match to occur.
The host has the IRK — mgmt, at bluetoothd startup:
The controller never receives it. Complete HCI sequence emitted on a connect attempt:
< HCI Command: LE Set Address Resolution Enable Address resolution: Disabled (0x00)
< HCI Command: LE Add Device To Accept List Address type: Random (0x01)
Address: AA:BB:CC:DD:EE:FF (Static)
< HCI Command: LE Set Address Resolution Enable Address resolution: Enabled (0x01)
< HCI Command: LE Set Extended Scan Parameters Own address type: Public (0x00)
Filter policy: Ignore not in accept list (0x01)
Type: Passive
< HCI Command: LE Set Extended Scan Enable Extended scan: Enabled (0x01)
... 20 seconds, zero advertising reports, no LE Extended Create Connection ...
Across a full 1487-line btmon capture spanning a bluetoothd restart plus a connect attempt there are zero LE Add Device To Resolving List commands. The resolving list is cleared at init (LE Clear Resolving List) and never repopulated.
The resulting state is unreachable-by-design: address resolution enabled against an empty resolving list, combined with accept-list filter policy for a peer that advertises only RPAs. The kernel took the LL-Privacy branch far enough to enable address resolution, but never programmed the key that branch depends on.
Pairing works because an unbonded connect has no IRK and no accept list — BlueZ connects straight to the RPA just observed in the scan.
Ruled out
Hypothesis
Verdict
Peripheral not advertising / not connectable
No — advertising at −42 dBm throughout; validated on Windows/Bumble/iOS/Android
Controller lacks LL Privacy
No — LE Read Local Supported Features reports LL Privacy and Extended Scanner Filter Policies
BlueZ main.conf misconfiguration
No — this kernel exposes only 3 mgmt experimental features (Simultaneous Central/Peripheral, ISO Socket, Vendor specific). LL Privacy is not among them, so KernelExperimental has nothing to enable
Stale or one-sided bond
No — Paired: yes, Bonded: yes, IRK loaded successfully over mgmt
Trusted flag routing into background auto-connect
Same broken path, not the cause — clearing it does not help
Client-side (bleak) bug
No — plain bluetoothctl connect fails identically; the scan resolves the peer in 0.2 s and the GATT DB is fully cached
Workaround (verified)
Hold a discovery session open, then connect normally:
nohup bluetoothctl --timeout 86400 scan le >/dev/null 2>&1&
hci_update_passive_scan_sync() returns early while discovery is active, so the kernel cannot install the filtered passive scan. Unfiltered reports keep flowing, the host resolves the RPA (it does hold the IRK), matches it against the pending connection, and Create Connection is issued to the currently-live RPA. Host-side resolution was never broken — only the controller-side path that never received the key.
Note this makes the bug invisible to any test that pairs and connects while discovery happens to be running.
Proposed kernel improvements
Items 1–3 target the defect; 4–6 are hardening and diagnostics.
Enforce one coherent privacy path. Inspect hci_le_add_accept_list_sync() and hci_le_add_resolve_list_sync() in net/bluetooth/hci_sync.c. Upstream guards the no-LL-Privacy case with roughly if (!use_ll_privacy(hdev) && hci_find_irk_by_addr(...)) return -EINVAL;, so the caller falls back to filter_policy = 0x00 (unfiltered passive scan plus host-side resolution). Our trace shows neither path completing: the accept list was used for an IRK-bearing RPA peer, and the resolving list was not programmed. Confirm whether that guard is present in this tree, and whether hci_le_add_resolve_list_sync() is returning early at its hci_find_irk_by_addr() lookup despite mgmt having loaded the key.
Add an invariant: never issue LE Set Address Resolution Enable unless at least one resolving-list entry was successfully programmed. The observed combination should be impossible to emit. A WARN_ON here would have surfaced this immediately instead of after a day of bisecting.
Restore or expose the LL Privacy mgmt experimental feature (UUID 15c0a148-c273-11ea-b3de-0242ac130004) if this tree dropped it, so BlueZ's KernelExperimental can enable controller-side resolution. If LL Privacy was removed deliberately, then the host-side fallback in item 1 becomes mandatory rather than optional.
Add a debugfs escape hatch, e.g. /sys/kernel/debug/bluetooth/hci0/force_unfiltered_passive_scan, to force filter_policy = 0x00. CONFIG_BT_DEBUGFS=y is already enabled; a runtime toggle would reduce this class of diagnosis to minutes and give benches a supported workaround instead of a permanently-open scan session.
Consider enabling CONFIG_BT_MSFTEXT (currently unset). Not required for this bug, but BlueZ actively drives advertisement-monitor offload (Remove Advertisement Monitor appears at startup) and MSFT extension support improves that path on capable controllers.
Add a bring-up regression test: bond to a privacy-enabled peripheral, disconnect, wait past one RPA rotation (Zephyr's default is 15 minutes), then reconnect with no discovery session active. A pair-then-connect test cannot catch this.
Verification plan
Reproduce on the current tree: bond, disconnect, connect with no discovery session → indefinite hang, zero advertising reports in btmon.
Apply candidate fix; confirm btmon shows either LE Add Device To Resolving List carrying the peer IRK, orFilter policy of 0x00 instead of Ignore not in accept list.
Confirm Create Connection is issued and LE Enhanced Connection Complete returns.
Confirm a GATT client connects with no external scan session.
Re-test after at least one RPA rotation, to prove the fix isn't a freshly-cached-RPA artifact.
Incidental observations
After bonding, BlueZ keeps the D-Bus object path derived from the peer's original RPA (/org/bluez/hci0/dev_<stale_RPA>) while patching its Address property to the identity address. Harmless, but confusing in traces; a bluetoothd restart re-keys it.
Concurrent clients interact badly and produce misleading errors: a stray disconnect cancels an in-flight connect (br-connection-canceled), and a pending connect makes subsequent attempts fail instantly with Operation already in progress. Neither reflects the real fault.
Warning
LLM Disclosure
Claude Code posting here, on behalf of @JPHutchins.
Prompt summary: A bench session that began as "help me pair a device with bluetoothctl" turned into a root-cause investigation of why BlueZ on our custom WSL2 kernel can pair with, but never reconnect to, a bonded privacy-enabled BLE peripheral. I was asked to file the findings — including proposed kernel-side improvements — as an issue here.
Model: Opus 5 (1M context) (
claude-opus-5[1m]).Summary
On our WSL2 host, BlueZ can pair with a privacy-enabled BLE peripheral but can never reconnect to it afterwards. Every post-bond connect hangs indefinitely and
LE Extended Create Connectionis never issued.HCI tracing shows the kernel enabling controller-side address resolution and filtering a passive scan on the accept list, while the controller's resolving list is empty. The peer's rotating RPA therefore can never match the accept-list entry (its static identity address), so no advertising reports are ever delivered and the pending connect never fires.
The peripheral is not at fault — the same device and firmware are validated over years against Windows, Bumble, iOS and Android. This is host-side.
A workaround exists and is verified (hold a discovery session open). A kernel-side fix is requested.
Environment
6.18.35.2-microsoft-standard-WSL2+(this tree)btusb+btmtk)bleak(BlueZ D-Bus backend) and plainbluetoothctl— both fail identicallyAddresses and keys below are anonymized:
AA:BB:CC:DD:EE:FF= peer identity address (static random).Reproduction
The peripheral advertises at −42 dBm throughout.
le-connection-abort-by-localis only the client canceling a still-pending connect, not the underlying failure.Root cause
BlueZ does not connect directly to a bonded LE peer. It programs the peer's identity address into the controller accept list, enables controller-side address resolution, starts a passive scan filtered on the accept list, and issues
Create Connectiononly once a matching advertiser is sighted. For a privacy peer, the controller must hold the peer's IRK in its resolving list for that match to occur.The host has the IRK — mgmt, at
bluetoothdstartup:The controller never receives it. Complete HCI sequence emitted on a connect attempt:
Across a full 1487-line
btmoncapture spanning abluetoothdrestart plus a connect attempt there are zeroLE Add Device To Resolving Listcommands. The resolving list is cleared at init (LE Clear Resolving List) and never repopulated.The resulting state is unreachable-by-design: address resolution enabled against an empty resolving list, combined with accept-list filter policy for a peer that advertises only RPAs. The kernel took the LL-Privacy branch far enough to enable address resolution, but never programmed the key that branch depends on.
Pairing works because an unbonded connect has no IRK and no accept list — BlueZ connects straight to the RPA just observed in the scan.
Ruled out
LE Read Local Supported FeaturesreportsLL PrivacyandExtended Scanner Filter Policiesmain.confmisconfigurationKernelExperimentalhas nothing to enablePaired: yes,Bonded: yes, IRK loaded successfully over mgmtTrustedflag routing into background auto-connectbleak) bugbluetoothctl connectfails identically; the scan resolves the peer in 0.2 s and the GATT DB is fully cachedWorkaround (verified)
Hold a discovery session open, then connect normally:
hci_update_passive_scan_sync()returns early while discovery is active, so the kernel cannot install the filtered passive scan. Unfiltered reports keep flowing, the host resolves the RPA (it does hold the IRK), matches it against the pending connection, andCreate Connectionis issued to the currently-live RPA. Host-side resolution was never broken — only the controller-side path that never received the key.Note this makes the bug invisible to any test that pairs and connects while discovery happens to be running.
Proposed kernel improvements
Items 1–3 target the defect; 4–6 are hardening and diagnostics.
Enforce one coherent privacy path. Inspect
hci_le_add_accept_list_sync()andhci_le_add_resolve_list_sync()innet/bluetooth/hci_sync.c. Upstream guards the no-LL-Privacy case with roughlyif (!use_ll_privacy(hdev) && hci_find_irk_by_addr(...)) return -EINVAL;, so the caller falls back tofilter_policy = 0x00(unfiltered passive scan plus host-side resolution). Our trace shows neither path completing: the accept list was used for an IRK-bearing RPA peer, and the resolving list was not programmed. Confirm whether that guard is present in this tree, and whetherhci_le_add_resolve_list_sync()is returning early at itshci_find_irk_by_addr()lookup despite mgmt having loaded the key.Add an invariant: never issue
LE Set Address Resolution Enableunless at least one resolving-list entry was successfully programmed. The observed combination should be impossible to emit. AWARN_ONhere would have surfaced this immediately instead of after a day of bisecting.Restore or expose the LL Privacy mgmt experimental feature (UUID
15c0a148-c273-11ea-b3de-0242ac130004) if this tree dropped it, so BlueZ'sKernelExperimentalcan enable controller-side resolution. If LL Privacy was removed deliberately, then the host-side fallback in item 1 becomes mandatory rather than optional.Add a debugfs escape hatch, e.g.
/sys/kernel/debug/bluetooth/hci0/force_unfiltered_passive_scan, to forcefilter_policy = 0x00.CONFIG_BT_DEBUGFS=yis already enabled; a runtime toggle would reduce this class of diagnosis to minutes and give benches a supported workaround instead of a permanently-open scan session.Consider enabling
CONFIG_BT_MSFTEXT(currently unset). Not required for this bug, but BlueZ actively drives advertisement-monitor offload (Remove Advertisement Monitorappears at startup) and MSFT extension support improves that path on capable controllers.Add a bring-up regression test: bond to a privacy-enabled peripheral, disconnect, wait past one RPA rotation (Zephyr's default is 15 minutes), then reconnect with no discovery session active. A pair-then-connect test cannot catch this.
Verification plan
btmon.btmonshows eitherLE Add Device To Resolving Listcarrying the peer IRK, orFilter policyof0x00instead ofIgnore not in accept list.Create Connectionis issued andLE Enhanced Connection Completereturns.Incidental observations
/org/bluez/hci0/dev_<stale_RPA>) while patching itsAddressproperty to the identity address. Harmless, but confusing in traces; abluetoothdrestart re-keys it.disconnectcancels an in-flight connect (br-connection-canceled), and a pending connect makes subsequent attempts fail instantly withOperation already in progress. Neither reflects the real fault.