44 · 回连 AUTH_FAILURE 方向分离定责法 — MIICOSBUG-4422
源码基线 f3dif(dev fa4a019f2)。交叉引用:32-双蓝牙上限与Picker(legacy picker)、66-bond状态与自动回连、40-日志取证速查。 本案价值:一套可复制的「回连/配对失败 0x05」定责方法论 + f3dif(mcb)picker 链路首案记录。
0. 结论先行(TL;DR)
- 现象:车机(faraday,A17,双栈)已连 2 台手机,iPhone 发起配对弹多手机替换框,选中断开 Xiaomi 15T;删 iPhone 后点回连 15T → 连续 4 次 toast「连接失败」,手机侧发起回连则成功。
- 根因:Xiaomi 15T 手机侧对车机发起的 ACL在鉴权+加密成功后 ~0.37s,伴随「MasterLinkKeyComplete(0x1B)+IOCapabilityReq(0x30)+RoleChange→master」突发,以 HCI 0x05 主动拆链。与替换框无因果(17:38 配对 ACL 在任何 picker 之前就被同签名拆掉)。
- 完美分离:失败 5/5 全是车机发起的 ACL;成功 2/2 全是手机发起的 ACL。
- JIRA 处置核验:resolution=不能修复/「车机无法规避」不准确——手机两次分别 0.6s/3.8s 后自愈,车机延迟重试即可让用户无感(可选兜底,非根因修复)。
- 证据底座:hci1 btsnoop 逐帧 + milog 轮转 gz + 主 txt 三源对齐;视频抽帧确认 toast 文案。
1. 为什么替换框最初像凶手(误导点)
- 现象紧邻替换框操作发生,测试步骤直接点名替换框;
- Triage 自动分析首版把 17:36 窗口的「disconnect D8」当成替换断开 → 实际那是用户手动 removeBond 清场,17:36 全窗无 picker;
- 「17:38 Xiaomi 15T previousBondState=BOND_NONE」看着像 bond 被替换流程弄丢 → 实际是该车机早就没和这台 15T 配对过(17:38:29 才由车机首次 createBond)。
教训:先重建完整时间线再谈因果。单点日志片段脱离上下文必然编出错误叙事。
2. 可复用方法论:0x05 回连失败四步定责法
第一步:分方向 —— 谁发起的 ACL,谁拆的链
btsnoop 里对每次失败/成功链路标两个方向:
- 发起方:链路以
TX CreateConn(0x0405)开头=本机发起;以RX ConnRequest(0x0C)+TX AcceptConnReq(0x0409)开头=对端发起。 - 拆链方:
DisconnectComplete(0x05)前有本机TX Disconnect(0x0406)= 本机拆;没有 = 对端拆(控制器代报 reason)。本机主动拆链 reason 常见 0x16(local user)/0x13(remote user 礼貌拆);0x05(Authentication Failure)出现在鉴权已成功之后 = 对端上层策略拆链,不是 key 不匹配。
本案:5 次失败链路全部是车机 CreateConn 发起、无 TX Disconnect、0x05;2 次成功链路全部是手机 ConnRequest 发起。方向分离 5/5 vs 2/2 = 定责对端的最强单证据。
第二步:link key 逐字节比对 —— 一票否决「key 不一致」类假设
对同一设备收集两类帧:
- 配对时刻的
LinkKeyNotify(EVT 0x18)(controller 上报新生成的 key) - 每次连接的
LinkKeyReply(CMD 0x040B)(host 应 controller 的 LinkKeyRequest 回复存储的 key)
逐字节相同 → 本端 key 存储一致,btif_storage 写坏/双栈错位/缓存污染类假设全部出局(且 AuthComplete status=0 本身已证明两端 key 一致——否则鉴权数学上就过不了)。本案 6 帧 key 全部=6751c07d...5d9d,H2/H3 一次性出局。
第三步:找拆链前的「决策标记」
失败链路与成功链路逐帧 diff,找只出现在失败侧的固定事件序列。本案拆链前 ~0.2s 必有:
MasterLinkKeyComplete(0x1B) → 事件0x30(IOCapabilityReq,对端要在已加密链路上重跑 SSP)→ RoleChange→master
5/5 有、2/2 无 → 不是随机/竞争,是对端确定性的链路管理决策(重协商失败后拆链)。注意甄别:RoleChange status=0x21(角色切换失败)也照拆 —— 说明拆链决策不依赖该仪式成功。
第四步:同文件内 A/B 对照豁免本端协议行为
怀疑「本端加密完成后发的 SDP/L2CAP 请求诱发对端拆链」时,检查成功链路上本端是否发了同一套流量。本案 17:38 手机发起的存活链路上,车机同样跑 SDP+RFCOMM(0x0003)+私有 PSM 0x1029,对方 ConnRsp 全 result=0 → 本端流量被豁免。
反陷阱清单
| 陷阱 | 正解 |
|---|---|
| 「Authentication Failure」字面带路去查 key | AuthComplete status=0 之后的 0x05 与 key 无关,是上层拆链 |
| 只看失败样本 | 必须找同设备同文件的成功对照组(方向/流量/仪式差异) |
| ACL Connection Already Exists = 竞争根因 | 本案唯一撞车那次恰恰成功;竞争指控必须有失败窗口内的竞争帧 |
| Triage/二手叙事的时间线 | 每条事件回原始日志核对(本案 17:36「替换断开」实为手动 unpair) |
| toast 文案当根因 | bluetooth_start_connect_device_failed 是通用回落提示,不含原因码 |
3. f3dif(mcb)picker 链路首案记录(A17 与 legacy 差异)
legacy picker(main 源集
MiBluetoothPickerDialogActivity)见 32 篇;A17/f3dif 起多手机场景由 mcb picker 接管,BluetoothPickerHooks.shouldShowLegacyMultiPhonePicker()=false(F3BluetoothPickerHookImpl.kt:19),legacy 五处拦截点全部短路。
f3dif 链路(本案实证):
- 触发:对端配对/主动连接致已连 HFP/A2DP 设备数 >
DOUBLE_BLUETOOTH_DEVICE_LIMIT(=2)→ mcb 拉起MultiPhonePickerDialogActivity(仅存在于 f3dif 源集,运行时 Activity 名可直接在 bugreport 里确认 flavor)。 - 候选过滤:
F3BluetoothPickerManager.filterMultiPhoneCandidates(f3dif/…/picker/F3BluetoothPickerManager.kt:321)——mcb 上报列表首位=新配对设备,候选=末尾 2 台已连设备(本案 size=3→2,候选=D8+15T,iPhone 被滤掉)。 - 选中执行:
MultiPhonePickerDialogFragment.onDeviceChosen(:114)→selectMultiPhoneDisconnect(:148)→F3BluetoothPickerManager.kt:501回传 mcbselectMultiPhoneDisconnectDevice(device)→ mcb 走 Platform_API37 →BluetoothAdapterServiceBinder.disconnectAllEnabledProfiles(milog 实证 uid 1001002)。 - 关键性质:整个 picker 源集无任何 removeBond/unpair 调用 —— 被替换设备只断 ACL(本案 0x16 本地干净拆除),bond 保留。所以「替换断开后列表显示已保存」是正确状态,回连理应免配对直连。
- 回连路径:
BluetoothBondedDevicesPrefController.java:266connectDevice →MiBluetoothExt.kt:19CachedBluetoothDevice.connect() → 栈侧 connectAllEnabledProfiles(多 profile 并发)。 - 失败 toast:
MiCarBluetoothBondedDevicePreference.kt:77-83——mStartToConnect && !isBusy && !isConnected时弹R.string.bluetooth_start_connect_device_failed(strings.xml:65)。通用回落提示,不含 HCI 原因码——看 toast 文案判断失败原因必走弯路,必须回 btsnoop。
4. 本案时间线(三源对齐后)
| 时间 | 事件 | 备注 |
|---|---|---|
| 17:36:33-40 | 用户手动 unpair 三台设备清场 | 非替换框!triage 叙事在此出错 |
| 17:37:29 | 车机点配 D8 → BONDED | |
| 17:38:29 | 车机点配 15T(此前 BOND_NONE) | createBond uid 1001000 |
| 17:38:31.8 | SC 配对成功,LinkKeyNotify key=6751c07d… | BONDED 31.884 |
| 17:38:32.2 | 配对 ACL 被手机 0x05 拆掉(带突发三件套) | 早于任何 picker |
| 17:38:32.9 | 手机主动回连 → 稳定 6 分钟 | 方向对照组 #1 |
| 17:44:17-20 | iPhone 手机侧配对 BONDED,已连数=3 | |
| 17:44:22 | mcb 弹 picker(候选=D8+15T) | MultiPhonePickerDialogActivity |
| 17:44:33 | 用户选中断开 15T → disconnectAllEnabledProfiles,bond 保留 | ACL 0x16 干净拆 |
| 17:45:04 | 用户删除 iPhone(removeBond) | 视频帧实证 |
| 17:45:05/13/17/24 | 车机 4 次点回连 15T → 全 0x05 | 帧级同签名 |
| 17:45:30 | 手机主动回连 → 成功至日志结束 | 方向对照组 #2 |
5. 处置与兜底
- 责任归属:手机侧(HyperOS 互联互通策略 vs 栈 bug,需 15T 手机侧 snoop 区分;两者同属手机侧)。车机栈/mcb picker/CarSettings 全链无责。
- 车机可选兜底(非根因修复):回连失败后延迟 3~5s 自动重试一次(实证手机 0.6s/3.8s 后自愈,用户可无感);或 toast 分层带失败原因(当前通用文案无法区分「对端拒绝」与「设备关机」)。
- JIRA 字段:
误报+不能修复+代码bug+进Gerrit四者至多取二;正确处置=转手机/OS4 团队(注明单号)+ 车机侧可选优化单。
6. 待取证(若再遇同类单)
- 对端手机 snoop(终裁 MiConnect 策略 vs 栈 bug);
- 非小米手机 A/B(确认是否 Xiaomi 15T/HyperOS 独占);
- 若对端是 iPhone/三星也复现 → 重新审视车机侧(本案证据链即不再成立,需重查)。
案件完整产物:
车机JIRA/jira-data/MIICOSBUG-4422/analysis/(root-cause.md + summary/code-refs/review.json);素材含 240MB bugreport(btsnoop 双栈+milog 轮转)+ 复现视频。