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)
PoC verification
Evidence pack
findings/poc/DF-5737 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | β | 4.4 KB | β raw | |
| verdict.json | β | 2.8 KB | view raw | |
| manifest.json | β | 409 B | view 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):
- Syscall thread (user context):
send(2)on an L2CAP socket βl2cap_send()βl2cap_lower.c:204hci_acl_send(m, chan->lc_link, chan)which walks/inserts onlink->hl_txq(sys/netbt/hci_link.c:585-586TAILQ_INSERT_TAIL,link->hl_txqlen += num) and callshci_acl_start(link). - NETISR thread (softint, mplock held): event handlers in
sys/netbt/hci_event.ctear links down: *hci_event_command_status()on a CREATE_CON status 0x12 frees every link inHCI_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.
Recommended fix
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_testableConfirmed 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.
No comments yet.