Skip to content

LE: cannot reconnect to bonded privacy (RPA) peripheral — address resolution enabled with an empty resolving list, accept-list scan filter can never match #3

Description

@JPHutchins

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 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

  1. Pair with a privacy-enabled LE peripheral (succeeds; bond and IRK stored).
  2. Disconnect.
  3. 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:

@ MGMT Command: Load Identity Resolving Keys (0x0030) plen 25
        Keys: 1
        LE Address: AA:BB:CC:DD:EE:FF (Static)
        Key: <redacted>
@ MGMT Event: Command Complete
      Load Identity Resolving Keys (0x0030) → Status: Success (0x00)

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 NoLE 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 NoPaired: 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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

  1. Reproduce on the current tree: bond, disconnect, connect with no discovery session → indefinite hang, zero advertising reports in btmon.
  2. Apply candidate fix; confirm btmon shows either LE Add Device To Resolving List carrying the peer IRK, or Filter policy of 0x00 instead of Ignore not in accept list.
  3. Confirm Create Connection is issued and LE Enhanced Connection Complete returns.
  4. Confirm a GATT client connects with no external scan session.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions