feat: reliable Bluetooth pairing, reconnection, and A2DP setup

Four fixes, each a root cause verified on hardware (AX211 + Bose QC Ultra):

1. Link Key Request Reply TRUNCATED: the pending-command queue's params
   buffer was 16 bytes; the reply is 22 (addr 6 + key 16).  The controller
   got 10 key bytes -> every stored-key reconnection failed authentication
   (status 5) since 2026-06-03 (c119a70).  Fresh pairings never touch this
   path, which kept the bug perfectly disguised as a headset quirk.

2. Secure Connections host support (0x0C7A) now enabled: bonds are minted
   as P-256 (Type=7), interoperable with BlueZ's, and SC-bonded peers can
   actually authenticate us.

3. Never write the BD_ADDR override (0xFC31) with the factory address:
   it desyncs the firmware's crypto address from the on-air one and ALL
   SSP pairing fails with status 5.  (The spoofing feature itself was
   already known-cosmetic: the baseband answers pages on the factory
   address regardless.)  import-bluez-bond.sh now removes the override.

4. A2DP channel setup: wait for Encryption Change before dialing L2CAP
   (post-SSP sinks ignore unencrypted CONN_REQ), and LISTEN 2.5s first --
   on reconnection the sink dials AVDTP itself and ignores our dials while
   doing so.  Ends the historical connRsp=FFFF retry-then-give-up failures.

Plus: queued security replies now log delivery + controller status.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 19:46:02 +02:00
parent 13a841d0f2
commit d9d32deb3e
6 changed files with 98 additions and 14 deletions
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once #pragma once
#define MONTAUK_BUILD_NUMBER 31 #define MONTAUK_BUILD_NUMBER 36
+59 -2
View File
@@ -971,9 +971,64 @@ namespace Drivers::USB::Bluetooth::A2dp {
return true; // channel configured + query sent; proceed to AVDTP return true; // channel configured + query sent; proceed to AVDTP
} }
// Wait for the ACL link to be ENCRYPTED before dialing any L2CAP channel.
// Post-SSP, the sink is REQUIRED to ignore unencrypted L2CAP -- dialing
// SDP/AVDTP before Encryption Change lands is a race we historically lost
// often (connRsp stays 0xFFFF on every attempt; "toggle the headphones"
// sometimes won the race by accident). Pump ProcessPendingCommands here:
// the SET_CONN_ENCRYPT that MAKES encryption happen sits in the pending
// queue and otherwise only goes out when the idle loop gets around to it.
static bool WaitEncrypted(uint32_t timeoutMs) {
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
Xhci::PollEvents();
Hci::DrainEvents();
Hci::ProcessPendingCommands();
auto* c = Hci::GetActiveConnection();
if (c && c->Encrypted) return true;
for (int j = 0; j < 100; j++) asm volatile("" ::: "memory");
}
return false;
}
bool StartSource(uint32_t timeoutMs) { bool StartSource(uint32_t timeoutMs) {
constexpr int kMaxAttempts = 4; constexpr int kMaxAttempts = 4;
if (WaitEncrypted(3000)) {
KernelLogStream(OK, "BT-A2DP") << "Link encrypted; dialing channels";
} else {
// Some sinks may not encrypt (legacy); proceed as before, but say so.
KernelLogStream(WARNING, "BT-A2DP")
<< "Link not encrypted after 3s; dialing anyway";
}
// Give the sink first move. On RECONNECTION (bonded device) sinks --
// Bose QC included -- commonly dial the AVDTP channels THEMSELVES
// right after encryption, and ignore the source's own CONN_REQs while
// their setup is in flight (observed: connRsp stays 0xFFFF on every
// dialed attempt, zero inbound traffic... because we never stopped
// transmitting long enough to receive). Listen briefly before
// dialing; fresh pairings are source-driven and just spend the wait.
g_sigCid = 0;
g_mediaCid = 0;
g_state = State::Idle;
g_txLabel = 1;
g_avdtpResponseReady = false;
{
uint64_t lStart = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - lStart < 2500) {
Xhci::PollEvents();
Hci::DrainEvents();
Hci::ProcessPendingCommands();
if (g_sigCid != 0) break;
for (int j = 0; j < 100; j++) asm volatile("" ::: "memory");
}
}
if (g_sigCid != 0) {
KernelLogStream(OK, "BT-A2DP") << "Sink opened AVDTP to us (cid="
<< base::hex << (uint64_t)g_sigCid << base::dec << ")";
}
// Connection phase, retried. A sink commonly ignores the very first // Connection phase, retried. A sink commonly ignores the very first
// L2CAP CONN_REQ that lands right after Encryption Change (connRsp stays // L2CAP CONN_REQ that lands right after Encryption Change (connRsp stays
// 0xFFFF, the headset never answers). Re-dial up to kMaxAttempts. We // 0xFFFF, the headset never answers). Re-dial up to kMaxAttempts. We
@@ -983,8 +1038,10 @@ namespace Drivers::USB::Bluetooth::A2dp {
// (connRsp!=0xFFFF) we do NOT loop -- that's the config phase, surfaced // (connRsp!=0xFFFF) we do NOT loop -- that's the config phase, surfaced
// by its own logs. We never reset the CID allocator, so each retry uses // by its own logs. We never reset the CID allocator, so each retry uses
// a fresh local CID and a late response for an old one cannot cross-wire. // a fresh local CID and a late response for an old one cannot cross-wire.
for (int attempt = 0; attempt < kMaxAttempts; attempt++) { // Dial only while no signaling channel exists in either direction;
g_sigCid = 0; // an inbound one (from the listen phase above, or landing between
// retries) short-circuits straight to negotiation.
for (int attempt = 0; attempt < kMaxAttempts && g_sigCid == 0; attempt++) {
g_mediaCid = 0; g_mediaCid = 0;
g_state = State::Idle; g_state = State::Idle;
g_txLabel = 1; g_txLabel = 1;
@@ -334,6 +334,18 @@ namespace Drivers::USB::Bluetooth {
// Enable Simple Secure Pairing // Enable Simple Secure Pairing
Hci::WriteSSPMode(1); Hci::WriteSSPMode(1);
// Enable Secure Connections host support (P-256 / AES-CCM). Link
// keys are procedure-bound: a bond minted over Secure Connections
// (BlueZ always negotiates SC -> key Type=7) CANNOT authenticate a
// legacy link -- the controller must fail with status 5. Without
// this, a key shared with a Linux install (dual boot, see
// scripts/import-bluez-bond.sh) is cryptographically fine yet
// unusable, and our own pairings mint legacy P-192 keys that Linux
// then silently replaces. Must follow Write SSP Mode.
uint8_t scOn = 0x01;
Hci::SendCommand(Hci::OP_WRITE_SC_HOST_SUPPORT, &scOn, 1);
Hci::WaitCommandComplete(Hci::OP_WRITE_SC_HOST_SUPPORT);
// Allow role switch + sniff on new connections. The controller default // Allow role switch + sniff on new connections. The controller default
// link policy is 0x0000 (deny both). A multipoint headset (Bose QC // link policy is 0x0000 (deny both). A multipoint headset (Bose QC
// Ultra) that also holds a link to a phone requests a role switch to // Ultra) that also holds a link to a phone requests a role switch to
+19 -4
View File
@@ -184,7 +184,12 @@ namespace Drivers::USB::Bluetooth::Hci {
// the DHKey check fail (Simple Pairing Complete status 0x05). Instead the // the DHKey check fail (Simple Pairing Complete status 0x05). Instead the
// handlers ENQUEUE the reply here; ProcessPendingCommands() sends it later // handlers ENQUEUE the reply here; ProcessPendingCommands() sends it later
// from top-level (process) context with a real, confirmed transfer. // from top-level (process) context with a real, confirmed transfer.
struct PendingHciCmd { uint16_t opcode; uint8_t len; uint8_t params[16]; }; // params must hold the LARGEST queued reply: Link Key Request Reply is
// 22 bytes (BD_ADDR 6 + key 16). This was 16, which silently TRUNCATED
// the link key reply to 10 key bytes -- every stored-key reconnection
// failed authentication (status 5) since the dawn of the driver; only
// fresh pairings (whose replies fit) ever worked.
struct PendingHciCmd { uint16_t opcode; uint8_t len; uint8_t params[22]; };
static PendingHciCmd g_pending[16] = {}; static PendingHciCmd g_pending[16] = {};
static volatile uint8_t g_pendingHead = 0; static volatile uint8_t g_pendingHead = 0;
static volatile uint8_t g_pendingTail = 0; static volatile uint8_t g_pendingTail = 0;
@@ -192,7 +197,7 @@ namespace Drivers::USB::Bluetooth::Hci {
static void EnqueueHciCmd(uint16_t opcode, const uint8_t* params, uint8_t len) { static void EnqueueHciCmd(uint16_t opcode, const uint8_t* params, uint8_t len) {
uint8_t next = (uint8_t)((g_pendingHead + 1) & 15); uint8_t next = (uint8_t)((g_pendingHead + 1) & 15);
if (next == g_pendingTail) return; // full -> drop (should never happen) if (next == g_pendingTail) return; // full -> drop (should never happen)
if (len > 16) len = 16; if (len > sizeof(g_pending[0].params)) len = sizeof(g_pending[0].params);
g_pending[g_pendingHead].opcode = opcode; g_pending[g_pendingHead].opcode = opcode;
g_pending[g_pendingHead].len = len; g_pending[g_pendingHead].len = len;
for (uint8_t i = 0; i < len; i++) g_pending[g_pendingHead].params[i] = params[i]; for (uint8_t i = 0; i < len; i++) g_pending[g_pendingHead].params[i] = params[i];
@@ -1271,13 +1276,23 @@ namespace Drivers::USB::Bluetooth::Hci {
while (g_pendingTail != g_pendingHead) { while (g_pendingTail != g_pendingHead) {
PendingHciCmd c = g_pending[g_pendingTail]; PendingHciCmd c = g_pending[g_pendingTail];
g_pendingTail = (uint8_t)((g_pendingTail + 1) & 15); g_pendingTail = (uint8_t)((g_pendingTail + 1) & 15);
SendCommand(c.opcode, c.params, c.len); bool sent = SendCommand(c.opcode, c.params, c.len);
// Set Connection Encryption returns Command Status (not Complete), // Set Connection Encryption returns Command Status (not Complete),
// so wait on that instead of burning a 1s timeout that delays A2DP. // so wait on that instead of burning a 1s timeout that delays A2DP.
if (c.opcode == OP_SET_CONN_ENCRYPT) { if (c.opcode == OP_SET_CONN_ENCRYPT) {
WaitCommandStatus(c.opcode, 1000); WaitCommandStatus(c.opcode, 1000);
} else { } else {
WaitCommandComplete(c.opcode, nullptr, 0, 1000); // Log delivery + the controller's verdict: security replies
// (link key / IO cap / confirm) failing HERE is invisible at
// the HCI level otherwise, and looks exactly like the remote
// rejecting authentication.
uint8_t st[1] = { 0xEE };
bool ok = WaitCommandComplete(c.opcode, st, 1, 1000);
KernelLogStream(INFO, "BT-HCI") << "queued cmd " << base::hex
<< (uint64_t)c.opcode << base::dec << " len=" << (uint64_t)c.len
<< " sent=" << (uint64_t)(sent ? 1 : 0)
<< " cc=" << (uint64_t)(ok ? 1 : 0)
<< " status=" << base::hex << (uint64_t)st[0] << base::dec;
} }
} }
s_active.store(false, std::memory_order_release); s_active.store(false, std::memory_order_release);
+1
View File
@@ -49,6 +49,7 @@ namespace Drivers::USB::Bluetooth::Hci {
constexpr uint16_t OP_WRITE_SCAN_ENABLE = 0x0C1A; constexpr uint16_t OP_WRITE_SCAN_ENABLE = 0x0C1A;
constexpr uint16_t OP_WRITE_CLASS_OF_DEVICE = 0x0C24; constexpr uint16_t OP_WRITE_CLASS_OF_DEVICE = 0x0C24;
constexpr uint16_t OP_WRITE_SSP_MODE = 0x0C56; constexpr uint16_t OP_WRITE_SSP_MODE = 0x0C56;
constexpr uint16_t OP_WRITE_SC_HOST_SUPPORT = 0x0C7A;
constexpr uint16_t OP_WRITE_INQUIRY_MODE = 0x0C45; constexpr uint16_t OP_WRITE_INQUIRY_MODE = 0x0C45;
constexpr uint16_t OP_WRITE_PAGE_TIMEOUT = 0x0C18; constexpr uint16_t OP_WRITE_PAGE_TIMEOUT = 0x0C18;
constexpr uint16_t OP_WRITE_AUTH_ENABLE = 0x0C20; constexpr uint16_t OP_WRITE_AUTH_ENABLE = 0x0C20;
+6 -7
View File
@@ -83,14 +83,13 @@ with open(path, "wb") as f:
print(f"wrote {path} ({len(blob)} bytes, 1 bond)") print(f"wrote {path} ({len(blob)} bytes, 1 bond)")
EOF EOF
# Pin the configured MAC to the factory address (truthful display; the 0xFC31 # Remove any BD_ADDR override: the controller must run its factory address
# override is cosmetic on this hardware anyway). # with NO 0xFC31 write. Verified on the AX211: writing 0xFC31 -- even with
# the factory address itself -- desyncs the address the firmware uses in SSP
# crypto from the one on air, and every pairing fails with status 5.
CONF="$TARGET/config/bluetooth.toml" CONF="$TARGET/config/bluetooth.toml"
mkdir -p "$TARGET/config"
if [ -f "$CONF" ] && grep -q '^ *mac *=' "$CONF"; then if [ -f "$CONF" ] && grep -q '^ *mac *=' "$CONF"; then
sed -i "s/^ *mac *=.*/mac = \"$ADAPTER\"/" "$CONF" sed -i '/^ *mac *=/d' "$CONF"
else echo "removed mac override from $CONF (0xFC31 breaks SSP on AX211)"
printf 'mac = "%s"\n' "$ADAPTER" >> "$CONF"
fi fi
echo "set $CONF mac = $ADAPTER"
echo "done — boot MontaukOS; the device should reconnect without re-pairing." echo "done — boot MontaukOS; the device should reconnect without re-pairing."