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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user