Pair() silently degrades to "No Bonding" when adapter is not pairable -- throwaway key deleted at drop, remote re-pair rejected 0x18; SetPairable intermittently fails org.bluez.Error.Busy

Cedrik Piehler <[email protected]> Mon, 3 Aug 2026 12:14:34 +0000
Newsgroups org.kernel.vger.linux-bluetooth
Message-ID <DB9PR08MB9802E0CD4A603984E8A7FEBF8FD52@DB9PR08MB9802.eurprd08.prod.outlook.com>
Component: bluetoothd + kernel mgmt pairing state machine (BR/EDR SSP bondi=
ng policy)=0A=
Version observed: BlueZ 5.87 (bluetoothctl 1.23), kernel 7.1.3-7.1.4 (x86-6=
4, Marvell/NXP IW620 BT via btusb)=0A=
Remote device: Sony DualSense (BR/EDR, SSP, requests Dedicated/General Bond=
ing)=0A=
Reproducibility: 100% when adapter is not pairable at Pair() time; 0% when =
it is (same boot, same adapter, same remote, minutes apart)=0A=
=0A=
Summary=0A=
-------=0A=
=0A=
Whether a Device1.Pair() produces a PERMANENT bond or a THROWAWAY link key =
depends entirely on the adapter's pairable flag at that instant -- but noth=
ing in the API surface tells the caller this, and three compounding behavio=
rs make the failure look random:=0A=
=0A=
1. Pairable silently turns itself off. With a default configuration (no exp=
licit PairableTimeout), pairable set via D-Bus was repeatedly observed gone=
 again ~30-60 s later, and also gone after daemon churn caused by disconnec=
ting/removing a previous bond.=0A=
=0A=
2. SetPairable can be rejected with org.bluez.Error.Busy while the daemon i=
s settling after a disconnect/remove -- so even re-asserting pairable immed=
iately before Pair() is not reliable unless the caller verifies the propert=
y afterwards.=0A=
=0A=
3. When pairable is off, Pair() still "succeeds" from the caller's perspect=
ive ("Pairing successful"), but the SSP exchange negotiated No Bonding (0x0=
1): the daemon stores a temporary link key, actively deletes it at the inev=
itable first disconnect (MGMT Unpair Device), and -- with pairable still of=
f -- answers the remote's own re-pair attempts with IO Capability Request N=
egative Reply, Reason: Pairing Not Allowed (0x18) until the remote gives up=
.=0A=
=0A=
Expected behavior=0A=
-----------------=0A=
=0A=
A client-initiated Pair() is an explicit bonding request. Either the daemon=
 should negotiate a bonding-capable authentication requirement for it regar=
dless of the pairable flag (pairable should gate INCOMING pairing, not an e=
xplicit outgoing Pair()), or it should fail loudly / document that Pair() r=
equires pairable to be set -- instead of returning success while producing =
a bond it deletes seconds later.=0A=
=0A=
Actual behavior (btmon-verified, A/B on the same boot)=0A=
-------------------------------------------------------=0A=
=0A=
Run A -- pairable OFF at Pair() time (fails):=0A=
=0A=
    @ MGMT Command: Set Bondable (0x0009)  Bondable: Disabled (0x00)   -> C=
urrent settings: 0x00400ac1   (Powered, SSP, BR/EDR, LE, SecureConn, LL Pri=
vacy -- NO Bondable)=0A=
    @ MGMT Command: Pair Device (0x0019)   Capability: KeyboardDisplay (0x0=
4)=0A=
    > HCI Event:  IO Capability Request (0x31)=0A=
    < HCI Command: IO Capability Request Reply  Authentication: No Bonding =
- MITM required (0x01)=0A=
    > HCI Event:  Simple Pairing Complete: Success=0A=
    > HCI Event:  Link Key Notification=0A=
    ... ~5 s later, link drops ...=0A=
    @ MGMT Command: Unpair Device          (daemon deletes the key it just =
made)=0A=
    # remote re-connects by itself (~+5 s, +11 s, +17 s); each time:=0A=
    > HCI Event:  Link Key Request -> Negative Reply (key already deleted)=
=0A=
    > HCI Event:  IO Capability Response (remote offers General Bonding 0x0=
4)=0A=
    < HCI Command: IO Capability Request Negative Reply  Reason: Pairing No=
t Allowed (0x18)=0A=
    > HCI Event:  Simple Pairing Complete: Authentication Failure (0x05) ->=
 Disconnect=0A=
=0A=
Run B -- pairable ON at Pair() time (works), minutes later, same everything=
:=0A=
=0A=
    @ MGMT Command: Set Bondable (0x0009)  Bondable: Enabled (0x01)    -> C=
urrent settings: 0x00400ad1   (... Bondable present)=0A=
    @ MGMT Command: Pair Device (0x0019)   Capability: KeyboardDisplay (0x0=
4)   # byte-identical to Run A=0A=
    > HCI Event:  IO Capability Request (0x31)=0A=
    < HCI Command: IO Capability Request Reply  Authentication: Dedicated B=
onding - MITM required (0x03)=0A=
    > HCI Event:  Simple Pairing Complete: Success=0A=
    > HCI Event:  Link Key Notification=0A=
    # link drops seconds later as with Run A -- but NO Unpair Device is eve=
r sent;=0A=
    # the remote re-connects and the bond is permanent. Zero 0x18 events.=
=0A=
=0A=
The Pair Device MGMT commands in both runs are byte-identical. The only dif=
ference is the bondable bit in the adapter settings at that moment.=0A=
=0A=
The SetPairable rejection (bluetoothctl output from a failing run):=0A=
=0A=
    [bluetoothctl]> pairable on=0A=
    Failed to set pairable on: org.bluez.Error.Busy=0A=
    hci0 new_settings: powered ssp br/edr le secure-conn ll-privacy     <- =
bondable absent=0A=
    [bluetoothctl]> pair D0:BC:C1:38:B2:7D=0A=
    Attempting to pair with D0:BC:C1:38:B2:7D=0A=
    ...=0A=
    Pairing successful        <- caller believes all is well; bond is a thr=
owaway per the trace above=0A=
=0A=
This run had issued "pairable on" ~40 s earlier (before a disconnect+remove=
 of the remote's previous bond and a 10 s scan). By Pair() time, pairable w=
as off again despite no client ever requesting that.=0A=
=0A=
Impact=0A=
------=0A=
=0A=
Any agent-driven pairing flow that (a) sets pairable more than ~30 s before=
 pairing, or (b) pairs after removing a stale bond, or (c) calls SetPairabl=
e while the daemon is busy -- i.e., most real-world re-pairing flows -- int=
ermittently produces bonds that self-destruct, with the failure surfaced se=
conds later and attributed by users to their hardware. Interactive "bluetoo=
thctl pair" succeeds or fails depending on how fast the operator types, whi=
ch made this extraordinarily hard to pin down (it looked like a timing/hard=
ware issue for weeks).=0A=
=0A=
Workaround (validated)=0A=
----------------------=0A=
=0A=
- PairableTimeout=3D0 in /etc/bluetooth/main.conf=0A=
- Set pairable AND verify (SetPairable, then read the property; retry while=
 org.bluez.Error.Busy) immediately before calling Pair(); re-assert after t=
he pairing session ends so remote-initiated re-pairs aren't rejected 0x18.=
=0A=
- With pairable confirmed on, the first bond is permanent (Dedicated Bondin=
g 0x03) and none of the fallback machinery is needed.=0A=
=0A=
Suggestions (as questions)=0A=
--------------------------=0A=
=0A=
1. Should an explicit Device1.Pair() really negotiate No Bonding? The calle=
r is, by definition, requesting a bond. Answering Dedicated/General Bonding=
 for an agent-initiated Pair() would make pairable a gate for incoming pair=
ing only, which matches user expectations.=0A=
2. If (1) is working as intended, could Pair() at least return a distinct e=
rror/warning when the result will be a throwaway key, instead of reporting =
success?=0A=
3. SetPairable -> org.bluez.Error.Busy during post-disconnect settling: cou=
ld this be queued/retried internally rather than rejected?=0A=
4. A short grace window before MGMT Unpair Device on a just-created No-Bond=
ing key (or re-using it when the same remote immediately attempts General B=
onding) would turn the failure mode into a success.=0A=
=0A=
Full decoded btmon traces of both runs available on request. Please keep me=
 in Cc on replies -- I am not subscribed to the list.=0A=