β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5738

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5738 Β· 15 files
FileTypeDescriptionSize
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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-5738

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.