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

Event-context link teardown races the syscall transmit path with no shared locking -> use-after-free write in hci_acl_send (forged RESET completion / CREATE_CON status / discon_compl frees links a send(2) is still touching)

Summary

Event handlers tear links down in NETISR context (command_status, con_compl error, discon_compl, and a forged RESET completion - accepted in steady state because completions are never matched to outstanding commands, dispatch is opcode-only) while a user thread in send(2) writes TAILQ_INSERT_TAIL into the same link: no lock serializes them and hl_refcnt only covers attached channels - the connecting window and per-call lc_link deref are uncovered -> write through a freed ~356-byte M_BLUETOOTH chunk (SMP slab-groomable, privesc ceiling). Not guest-verifiable without invalidating the verbatim-code rig - honestly speculative with the full static chain; refcount-across-transmit + completion-matching fix proposed.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5737 Β· 3 files
FileTypeDescriptionSize
VERDICT.md β€” 4.4 KB ↓ raw
verdict.json β€” 2.8 KB view raw
manifest.json β€” 409 B view raw
VERDICT.md
↓ download raw

DF-5737 β€” event-context link teardown races the syscall transmit path (no locking) β†’ UAF write

File: sys/netbt/hci_event.c (pass 2) triggers; sink across sys/netbt/hci_link.c / l2cap_lower.c Severity: Medium (speculative/likely β€” statically argued, not guest-verified) CWE-362 / CWE-416

Analysis (why this is a real race)

Two threads touch struct hci_link state concurrently with no shared lock (netbt uses only crit_enter() around list ops, which does not serialize against other threads on SMP, and the event path drops hci_devlock before dispatch):

  1. Syscall thread (user context): send(2) on an L2CAP socket β†’ l2cap_send() β†’ l2cap_lower.c:204 hci_acl_send(m, chan->lc_link, chan) which walks/inserts on link->hl_txq (sys/netbt/hci_link.c:585-586 TAILQ_INSERT_TAIL, link->hl_txqlen += num) and calls hci_acl_start(link).
  2. NETISR thread (softint, mplock held): event handlers in sys/netbt/hci_event.c tear links down: * hci_event_command_status() on a CREATE_CON status 0x12 frees every link in HCI_LINK_WAIT_CONNECT (sys/netbt/hci_event.c:271-273); * hci_event_con_compl() frees the looked-up link on error status (sys/netbt/hci_event.c:559); * hci_event_discon_compl() frees by handle (sys/netbt/hci_event.c:629-631); * hci_cmd_reset() (a forged RESET Command_Complete, accepted at any time β€” no correlation with an outstanding command) frees all links on the unit (sys/netbt/hci_event.c:1063-1070).

hci_link_free() (sys/netbt/hci_link.c:931-1025) closes L2CAP channels (dropping their hl_refcnt references) and then kfree()s the link.

The reference scheme only protects links that L2CAP channels hold references to (hl_refcnt). But the transmit path's chan->lc_link raw pointer is dereferenced in hci_acl_send() before any refcount is taken per-call: if the event thread frees the link between the socket thread's chan->lc_link load and its TAILQ_INSERT_TAIL(&link->hl_txq, ...), the insert writes through the freed struct hci_link (~356-byte M_BLUETOOTH allocation) β€” a use-after-free write with partly attacker-shaped content (the l2cap_pdu list pointers). For links in HCI_LINK_WAIT_CONNECT (channels are still attaching; hl_refcnt transitions in hci_acl_open() at sys/netbt/hci_link.c:143) the window is widest: a forged CREATE_CON status-0x12 event frees the link while the connecting syscall is between hci_send_cmd() (:109) and link->hl_refcnt++ (:143).

Note the command-completion path also has no matching of completions to outstanding commands (hci_event_command_compl() dispatches purely on the opcode in the event, sys/netbt/hci_event.c:324-351), so a forged RESET completion is accepted in steady state β€” i.e. the trigger does not require the controller to actually reset.

Why untested on the guest

Proving the interleaving requires winning a narrow window between a syscall thread and the NETISR thread on the 6-CPU guest with no instrumentation hook in the production transmit path (the harness rig injects events but cannot freeze a userland syscall mid-hci_acl_send without patching the code under test, which would invalidate the "verbatim" property). A deterministic demonstration would require a second KLD stubbing hci_acl_send's internals β€” that demonstrates the harness, not the race. Filed as speculative/likely with the static chain above; the recommended fix (refcount the link across the transmit call) removes the class regardless.

Take an explicit reference around the transmit call:

 /* l2cap_lower.c */
 hci_link_ref(chan->lc_link);          /* new: +1 unless being freed */
 err = hci_acl_send(m, chan->lc_link, chan);
 hci_acl_close(chan->lc_link, err);

plus converting hci_link_free() calls in the event handlers to go through hci_acl_close() (or otherwise respect hl_refcnt) so a referenced link is deferred, and gating hci_cmd_reset()'s teardown on an outstanding RESET command.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean
DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

attacker injects forged HCI event (e.g. RESET completion) while local L2CAP socket send() is inside hci_acl_send() -> link freed under the syscall thread -> TAILQ_INSERT_TAIL writes through freed 356-byte M_BLUETOOTH chunk (slab-groomable in principle)

Evidence (decisive lines)

VERDICT.md (full static chain with path:line for both threads and the windows)

PoC changes

no runnable PoC (race window not instrumentable without patching the code under test)

Verified recommended fix

hold an hl_refcnt reference across hci_acl_send() in l2cap_lower.c; make event-path teardown go through hci_acl_close(); match command completions to outstanding commands before acting

Verdict

Statically argued use-after-free WRITE: netbt has no lock serializing the syscall transmit path (l2cap_lower.c:204 -> hci_acl_send -> TAILQ_INSERT_TAIL on link->hl_txq, hci_link.c:585) against event-context link teardown in hci_event.c (command_status CREATE_CON 0x12 at :271-273, con_compl error at :559, discon_compl at :629, forged RESET completion at :1063-1070 -> hci_link_free -> kfree). hl_refcnt only protects channels already attached; the connecting window (hci_acl_open hci_link.c:109-143) and the per-call lc_link dereference are uncovered. Forged completions need no matching outstanding command (dispatch is opcode-only, hci_event.c:324-351). Not guest-verified: winning the syscall-vs-NETISR interleaving needs instrumentation inside the verbatim code under test, which would invalidate the rig; documented as speculative/likely with the full chain.