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 = 定责对端的最强单证据。

对同一设备收集两类帧:

  • 配对时刻的 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」字面带路去查 keyAuthComplete 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 链路(本案实证):

  1. 触发:对端配对/主动连接致已连 HFP/A2DP 设备数 > DOUBLE_BLUETOOTH_DEVICE_LIMIT(=2) → mcb 拉起 MultiPhonePickerDialogActivity(仅存在于 f3dif 源集,运行时 Activity 名可直接在 bugreport 里确认 flavor)。
  2. 候选过滤:F3BluetoothPickerManager.filterMultiPhoneCandidates(f3dif/…/picker/F3BluetoothPickerManager.kt:321)——mcb 上报列表首位=新配对设备,候选=末尾 2 台已连设备(本案 size=3→2,候选=D8+15T,iPhone 被滤掉)。
  3. 选中执行:MultiPhonePickerDialogFragment.onDeviceChosen(:114)→ selectMultiPhoneDisconnect(:148)→ F3BluetoothPickerManager.kt:501 回传 mcb selectMultiPhoneDisconnectDevice(device) → mcb 走 Platform_API37 → BluetoothAdapterServiceBinder.disconnectAllEnabledProfiles(milog 实证 uid 1001002)。
  4. 关键性质:整个 picker 源集无任何 removeBond/unpair 调用 —— 被替换设备只断 ACL(本案 0x16 本地干净拆除),bond 保留。所以「替换断开后列表显示已保存」是正确状态,回连理应免配对直连。
  5. 回连路径:BluetoothBondedDevicesPrefController.java:266 connectDevice → MiBluetoothExt.kt:19 CachedBluetoothDevice.connect() → 栈侧 connectAllEnabledProfiles(多 profile 并发)。
  6. 失败 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.8SC 配对成功,LinkKeyNotify key=6751c07d…BONDED 31.884
17:38:32.2配对 ACL 被手机 0x05 拆掉(带突发三件套)早于任何 picker
17:38:32.9手机主动回连 → 稳定 6 分钟方向对照组 #1
17:44:17-20iPhone 手机侧配对 BONDED,已连数=3
17:44:22mcb 弹 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. 待取证(若再遇同类单)

  1. 对端手机 snoop(终裁 MiConnect 策略 vs 栈 bug);
  2. 非小米手机 A/B(确认是否 Xiaomi 15T/HyperOS 独占);
  3. 若对端是 iPhone/三星也复现 → 重新审视车机侧(本案证据链即不再成立,需重查)。

案件完整产物:车机JIRA/jira-data/MIICOSBUG-4422/analysis/(root-cause.md + summary/code-refs/review.json);素材含 240MB bugreport(btsnoop 双栈+milog 轮转)+ 复现视频。