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
+59 -2
View File
@@ -971,9 +971,64 @@ namespace Drivers::USB::Bluetooth::A2dp {
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) {
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
// L2CAP CONN_REQ that lands right after Encryption Change (connRsp stays
// 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
// 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.
for (int attempt = 0; attempt < kMaxAttempts; attempt++) {
g_sigCid = 0;
// Dial only while no signaling channel exists in either direction;
// 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_state = State::Idle;
g_txLabel = 1;