hci_link_free() ignores the SCO->ACL anchor reference: anchored ACL teardown leaves hl_refcnt==1 -> KKASSERT panic on INVARIANTS kernels, silent kfree + dangling SCO->hl_link (fatal UAF demonstrated) on production GENERIC kernels
Summary
hci_link_free drops only L2CAP-channel references; the reference a SCO link holds via hl_link (taken by the production hci_acl_open inside hci_sco_newconn/sco_connect) is never released - only hci_cmd_reset does that dance (its comment proves the authors knew). An ACL Disconnect_Complete while a SCO link is anchored leaves refcnt==1: KKASSERT panic on INVARIANTS, and on production GENERIC (KKASSERT compiled out) a SILENT kfree with the SCO link still pointing at it - subsequent SCO teardown is a UAF write. Guest-verified deterministically: SCO-first control clean, ACL-first ordering crashed (Fatal trap 12 supervisor write inside hci_link_free); fix validated (SCO-anchor release pass, 0/256 spray canaries touched). A hostile controller orders the disconnects at will; conforming controllers may too.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5738 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | β | 24.2 KB | view raw | |
| hci_stub.c | β | 5.0 KB | view raw | |
| stub.h | β | 281 B | view raw | |
| Makefile | β | 530 B | β download | |
| build.sh | β | 116 B | view raw | |
| run.sh | β | 180 B | view raw | |
| fixbuild.sh | β | 588 B | view raw | |
| build.log | β | 1.0 KB | view raw | |
| run.log | β | 3.6 KB | view raw | |
| panic.txt | β | 3.1 KB | view raw | |
| fix.diff | β | 3.0 KB | view raw | |
| fix_run.log | β | 3.3 KB | view raw | |
| env.txt | β | 732 B | view raw | |
| verdict.json | β | 5.1 KB | view raw | |
| manifest.json | β | 1.3 KB | view raw |
Fix verification
fixedfix.diff adds a SCO-anchor release pass inside hci_link_free() (plus, shared with DF-5739, an exactly-once atomic teardown guard and callout_drain/terminate). Applied to the guest's /usr/src copy, module rebuilt, identical trigger re-run: SCO anchor released inside the ACL teardown (sco_disconnected=2), both links freed (links=0), no trap, and the spray-reclaim canary scan found 0/256 modifications - the freed-memory write is gone. Baseline crashed inside this exact sequence.
fix_run.log (patched-module transcript); fix.diff; build.log
Confirmed kernel references
Detail
Exploit chain
hostile/quirky controller emits Disconnection_Complete(ACL) while a SCO link is anchored (nothing lets the host rely on controller ordering; a hostile transport chooses it at will; hci_cmd_reset's SCO-first comment proves the dependency is real) -> hci_link_free(ACL) with hl_refcnt==1 -> [GENERIC] silent kfree, SCO->hl_link dangles -> next SCO activity (sco_disconnect / discon_compl(SCO) -> hci_link_free(SCO) -> hci_acl_close(freed)) writes into freed/reused 168-byte M_BLUETOOTH memory (demonstrated: fatal trap) -> with heap grooming of the bluetooth slab (connection churn is attacker-paced) this is a UAF-write primitive; not pushed to uid=0 (no Bluetooth hardware; KLD rig demonstrates the primitive). [INVARIANTS] immediate DoS panic.
Evidence (decisive lines)
panic.txt: full serial console - control (SCO-first) clean, then '2) ACL#5 freed ... DANGLING', then Fatal trap 12 supervisor WRITE in hci_link_free+0x2cb. run.log: complete transcript with refcnt accounting (anchor=1). fix_run.log: patched module, same sequence, links=0, sco_disconnected=2, 0/256 spray canaries, no trap.
PoC changes
Extended the DF-5733/5734 KLD rig pattern to the SCO paths: hci_stub.c gains a listening sco_pcb + btproto whose newconn op returns a stub pcb so the FULL production hci_sco_newconn() -> hci_acl_open() path runs verbatim; harness adds hlh_sco_anchor (con_req(SCO) event), hlh_fire_discon (discon_compl events, v=2 SCO-first control), hlh_uaf (anchor -> kill ACL -> 256x sizeof(hci_link) 0xA5 spray reclaim -> SCO teardown -> canary scan for the --freed->hl_refcnt write). Fake device_t changed from a raw 0xcafe0000 constant to a mapped blob with a valid nameunit string because production kprintf paths (hci_acl_recv:473 etc.) dereference it.
Verified recommended fix
Release SCO links anchored via hl_link at the top of hci_link_free() before the refcount assertion (mirror hci_cmd_reset's SCO-first pass) - see fix.diff.
Verdict
hci_link_free() only drops L2CAP-channel references before demanding hl_refcnt==0; the SCO->ACL anchor reference taken by production hci_acl_open() (from hci_sco_newconn(), hci_link.c:810, or sco_upper.c:160) is never released, so a Disconnect_Complete for an ACL link with a live SCO anchor leaves hl_refcnt==1. On INVARIANTS kernels this is an immediate KKASSERT(hl_refcnt==0) panic (hci_link.c:952); on production GENERIC kernels KKASSERT is compiled out (systm.h:98-118) and the link is silently kfree'd while the SCO link and its pcb still reference it. Verified on the guest with the verbatim-production KLD rig: SCO-first ordering (the order hci_cmd_reset assumes) is clean; ACL-first ordering deterministically leaves the SCO->hl_link dangling and the subsequent production SCO teardown crashed the kernel with a supervisor-write fault inside hci_link_free (Fatal trap 12, 'Stopped at hci_link_free+0x2cb: movq %rdx,0x78(%rax)'). fix.diff (SCO-anchor release pass mirroring hci_cmd_reset) validated: identical sequence completes cleanly, disconnected callback fires exactly once, 0/256 spray-reclaim canaries touched, guest alive.
No comments yet.