β¬’ DragonFlyBSD Kernel Audit
← dashboard
DF-0586

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

  1. The list is a bare LIST_HEAD with 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).

  2. The only synchronization on the insert path is a DragonFly crit_enter() section around LIST_INSERT_HEAD in hci_sattach (sys/netbt/hci_socket.c:655-657). On DragonFly, crit_enter() only blocks local-CPU preemption/interrupts and provides no cross-CPU exclusion.

  3. The remove path in hci_sdetach has no protection at all: LIST_REMOVE(pcb, hp_next); kfree(pcb, M_PCB); (sys/netbt/hci_socket.c:576-577).

  4. The traversal path in hci_mtap uses bare LIST_FOREACH over the same list (sys/netbt/hci_socket.c:935) and, for each entry, reads pcb->hp_flags, pcb->hp_laddr, pcb->hp_efilter, pcb->hp_pfilter, and &pcb->hp_socket->so_rcv.sb, then calls sbappendaddr/sorwakeup on it (sys/netbt/hci_socket.c:1004-1006).

  5. hci_mtap is reachable both from the bluetooth netisr input path (which holds the MP lock via get_mplock() in sys/netbt/bt_input.c:36) and from hci_send β†’ hci_output_cmd (sys/netbt/hci_socket.c:533 β†’ sys/netbt/hci_unit.c:501-515) in the pru_send context, which does not take the MP lock. hci_sdetach runs in pru_detach context, also without the MP lock. A grep across sys/netbt confirms the only lock in the entire netbt subsystem is unit->hci_devlock (a per-unit device-queue lock, sys/netbt/hci_unit.c:102) β€” there is no pcb-list lock.

  6. The same lockless-pattern concern applies to hci_cmdwait_flush (sys/netbt/hci_socket.c:455-463) walking the global hci_unit_list while a unit can be concurrently TAILQ_REMOVE'd by hci_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; the HCI_PRIVILEGED flag is only set when caps_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_ubt module so the unit list is non-empty and hci_mtap has a packet to tap).
  • Reachability: attacker opens two HCI sockets, binds one to a real unit (via SIOCGBTINFOA), floods sendto() to drive the binder's hci_send β†’ hci_output_cmd β†’ hci_mtap traversal, while concurrently close()ing the second socket to trigger hci_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_send and pru_detach happen 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.

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_EXCLUSIVE sleep 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).