Lockless global hci_pcb list allows use-after-free during concurrent socket teardown and packet tap
| Field | Value |
|---|---|
| ID | DF-0586 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-416 Use After Primary Resource; CWE-362 Concurrent Execution using Shared Resource with Improper Synchronization |
| File | sys/netbt/hci_socket.c |
| Lines | 87, 455-463, 556-577, 655-657, 935-1011 |
| Area | netbt (Bluetooth subsystem) |
| Confidence | speculative |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
The global hci_pcb list of Bluetooth HCI raw-socket protocol control blocks is
traversed and mutated without any cross-CPU lock. hci_mtap
(sys/netbt/hci_socket.c:935) walks the list with LIST_FOREACH and dereferences
each pcb (including pcb->hp_socket->so_rcv.sb via sbappendaddr at
:1004-1006), while hci_sdetach (sys/netbt/hci_socket.c:576-577) removes an
entry and calls kfree(pcb, M_PCB) with no protection at all. A concurrent
socket close on another CPU can free a pcb out from under a concurrent
packet-tap traversal, producing a use-after-free read on the freed pcb, a
use-after-free write via sbappendaddr/sorwakeup on the freed/reused
hp_socket, or a NULL/EAR dereference on LIST_NEXT (kernel panic).
Root cause
-
The list is a bare
LIST_HEADwith no lock initializer or mutex:LIST_HEAD(hci_pcb_list, hci_pcb) hci_pcb = LIST_HEAD_INITIALIZER(hci_pcb);(sys/netbt/hci_socket.c:87). -
The only synchronization on the insert path is a DragonFly
crit_enter()section aroundLIST_INSERT_HEADinhci_sattach(sys/netbt/hci_socket.c:655-657). On DragonFly,crit_enter()only blocks local-CPU preemption/interrupts and provides no cross-CPU exclusion. -
The remove path in
hci_sdetachhas no protection at all:LIST_REMOVE(pcb, hp_next); kfree(pcb, M_PCB);(sys/netbt/hci_socket.c:576-577). -
The traversal path in
hci_mtapuses bareLIST_FOREACHover the same list (sys/netbt/hci_socket.c:935) and, for each entry, readspcb->hp_flags,pcb->hp_laddr,pcb->hp_efilter,pcb->hp_pfilter, and&pcb->hp_socket->so_rcv.sb, then callssbappendaddr/sorwakeupon it (sys/netbt/hci_socket.c:1004-1006). -
hci_mtapis reachable both from the bluetooth netisr input path (which holds the MP lock viaget_mplock()in sys/netbt/bt_input.c:36) and fromhci_sendβhci_output_cmd(sys/netbt/hci_socket.c:533 β sys/netbt/hci_unit.c:501-515) in thepru_sendcontext, which does not take the MP lock.hci_sdetachruns inpru_detachcontext, also without the MP lock. A grep acrosssys/netbtconfirms the only lock in the entire netbt subsystem isunit->hci_devlock(a per-unit device-queue lock, sys/netbt/hci_unit.c:102) β there is no pcb-list lock. -
The same lockless-pattern concern applies to
hci_cmdwait_flush(sys/netbt/hci_socket.c:455-463) walking the globalhci_unit_listwhile a unit can be concurrentlyTAILQ_REMOVE'd byhci_detach(sys/netbt/hci_unit.c:126).
Because nothing in the code β no lock, no port-requirement flag in btsw[] at
sys/netbt/bt_proto.c:73 β pins pru_send and pru_detach for distinct sockets
onto the same CPU message port, two sockets on different CPUs can race.
Threat model & preconditions
- Attacker position: local unprivileged user.
socket(PF_BLUETOOTH, BTPROTO_HCI)is attachable by any user (sys/netbt/hci_socket.c:619; theHCI_PRIVILEGEDflag is only set whencaps_priv_check_self(SYSCAP_RESTRICTEDROOT)succeeds at :644, but the socket itself is created regardless). - Privileges gained or impact: realistic worst case is local privilege escalation via heap-grooming; minimum reliable case is local kernel panic (DoS).
- Required config or capabilities: SMP DragonFly system with a Bluetooth
controller present (or a loaded
ubt(4)/ng_ubtmodule so the unit list is non-empty andhci_mtaphas a packet to tap). - Reachability: attacker opens two HCI sockets, binds one to a real unit
(via
SIOCGBTINFOA), floodssendto()to drive the binder'shci_sendβhci_output_cmdβhci_mtaptraversal, while concurrentlyclose()ing the second socket to triggerhci_sdetachβkfree.
Proof of concept
PoC source: findings/poc/DF-0586/poc_race.c
Build & run
cc -o poc_race findings/poc/DF-0586/poc_race.c -lpthread ./poc_race
Expected output
A kernel panic with a faulting instruction inside hci_mtap
(sys/netbt/hci_socket.c:~935-1006) or sbappendaddr, e.g.:
Fatal trap 12: page fault while in kernel mode cpuid = 1; apic id = 01 fault virtual address = 0xdeadbeef... [code] hci_mtap+0x...: ...
For the escalation variant (heap-grooming spray of sizeof(struct hci_pcb)
objects to reclaim the freed slab and turn sbappendaddr into a controlled
write into a victim object), see findings/poc/DF-0586/VERDICT.md after PoC
verification β the precise slab-size analysis and grooming recipe are
materialized by the per-PoC verifier.
Impact
- Blast radius: any DragonFly system exposing Bluetooth HCI sockets to unprivileged users (default config on systems with a Bluetooth controller).
- Severity rationale: Medium. Local-only, high-complexity race window
(the speculative component reflects dependency on per-protocol message-port
CPU affinity β if
pru_sendandpru_detachhappen to be serialized onto the same CPU the race is harder), but worst-case impact is local unprivβroot via heap reuse, and minimum reliable impact is local unpriv DoS (panic). - Reliability: speculative at filing time; concrete reproducibility to be established by the per-PoC verifier on a live DragonFly guest.
Recommended fix
Add an explicit lock around the global hci_pcb list and hold it across every
traversal and mutation. The cleanest fix matching the rest of DragonFly netbt
(which already uses a struct lock for unit->hci_devlock) is a dedicated lock
initialised at domain setup and acquired around hci_sattach insert,
hci_sdetach remove, and the full hci_mtap/hci_cmdwait_flush traversals.
The LIST_FOREACH body in hci_mtap only reads pcb fields and appends to
per-socket rcv buffers (which are individually locked by
sbappendaddr/sorwakeup), so an LK_SHARED lock over the traversal plus an
LK_EXCLUSIVE over hci_sdetach's LIST_REMOVE is sufficient to close the
race. If a shared/exclusive lock is considered too heavy for the input hot
path, an equally-valid minimal fix is a single exclusive spinlock acquired
briefly in hci_sattach, hci_sdetach, and held for the duration of the
hci_mtap LIST_FOREACH.
--- a/sys/netbt/hci_socket.c
+++ b/sys/netbt/hci_socket.c
@@ -84,6 +84,8 @@
LIST_HEAD(hci_pcb_list, hci_pcb) hci_pcb = LIST_HEAD_INITIALIZER(hci_pcb);
+struct lock hci_pcb_lock = LOCK_INITIALIZER("hci_pcb", 0, 0);
+
/* sysctl defaults */
int hci_sendspace = HCI_CMD_PKT_SIZE;
int hci_recvspace = 4096;
@@ -452,6 +454,7 @@ hci_send(struct socket *so, struct mbuf *m, struct sockaddr *dstaddr,
static void
hci_cmdwait_flush(struct socket *so)
{
+ lockmgr(&hci_pcb_lock, LK_SHARED);
TAILQ_FOREACH(unit, &hci_unit_list, hci_next) {
IF_POLL(&unit->hci_cmdwait, m);
while (m != NULL) {
@@ -462,6 +465,7 @@ hci_cmdwait_flush(struct socket *so)
m = m->m_nextpkt;
}
}
+ lockmgr(&hci_pcb_lock, LK_RELEASE);
}
@@ -564,6 +568,7 @@ hci_sdetach(netmsg_t msg)
so->so_pcb = NULL;
sofree(so); /* remove pcb ref */
+ lockmgr(&hci_pcb_lock, LK_EXCLUSIVE);
LIST_REMOVE(pcb, hp_next);
+ lockmgr(&hci_pcb_lock, LK_RELEASE);
kfree(pcb, M_PCB);
error = 0;
}
@@ -652,9 +657,9 @@ hci_sattach(netmsg_t msg)
hci_filter_set(HCI_EVENT_COMMAND_STATUS, &pcb->hp_efilter);
hci_filter_set(HCI_EVENT_PKT, &pcb->hp_pfilter);
- crit_enter();
+ lockmgr(&hci_pcb_lock, LK_EXCLUSIVE);
LIST_INSERT_HEAD(&hci_pcb, pcb, hp_next);
- crit_exit();
+ lockmgr(&hci_pcb_lock, LK_RELEASE);
error = 0;
out:
@@ -933,6 +938,7 @@ hci_mtap(struct hci_unit *unit, struct mbuf *m)
sa.bt_family = AF_BLUETOOTH;
bdaddr_copy(&sa.bt_bdaddr, &unit->hci_bdaddr);
+ lockmgr(&hci_pcb_lock, LK_SHARED);
LIST_FOREACH(pcb, &hci_pcb, hp_next) {
/*
* filter according to source address
@@ -1006,6 +1012,7 @@ hci_mtap(struct hci_unit *unit, struct mbuf *m)
m_freem(m0);
}
}
+ lockmgr(&hci_pcb_lock, LK_RELEASE);
}
References
- DragonFlyBSD
crit_enter(9): blocks local-CPU preemption only, not other CPUs. - DragonFlyBSD
lockmgr(9):LK_SHARED/LK_EXCLUSIVEsleep lock. - FreeBSD rS190271 / netgraph serialization model: alternative
NG_NODE_FORCE_WRITER-style serialization as a defence-in-depth pattern (compare sys/netgraph7/bridge/ng_bridge.c, which relies on writer serialization for the same class of list/lifetime safety).
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).