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).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0586 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df0586_harness.c | trigger-source | kld harness: kernel thread that calls hci_mtap() in a loop with a synthesized unit+mbuf (stands in for the missing BT-radio input path); also a self-contained walker+killer mode for stress | 8.5 KB | view raw |
| poc_race.c | trigger-source | userspace dense driver: 4 churner threads (64 sockets each, continuous close+reopen) + 2 spammer threads; drives the real hci_sattach/hci_sdetach | 3.3 KB | view raw |
| build.sh | build-script | exact cc/Makefile build for both artifacts | 625 B | view raw |
| run.sh | run-script | checks harness is loaded then runs poc_race 30 | 696 B | view raw |
| fix.diff | suggested-fix | git-apply-able minimal fix: adds hci_pcb_lock, LK_EXCLUSIVE around hci_sattach LIST_INSERT_HEAD and hci_sdetach LIST_REMOVE, LK_SHARED around hci_mtap LIST_FOREACH | 1.3 KB | view raw |
| build.log | build-log | full untrimmed sh build.sh output on the unpatched #0 baseline | 3.6 KB | view raw |
| run.log | run-log | 20s userspace-only run on #0 baseline: ~5M churner+spammer cycles, harness NOT loaded, no panic | 509 B | view raw |
| run.2.log | run-log | 8s run with harness loaded (walker-only mode) on #0 baseline: 1.8M close+reopen + 793K spammer cycles + 15M hci_mtap walker cycles, no panic | 507 B | view raw |
| fix_build.log | build-log | full untrimmed 'make -j6 nativekernel' output on the patched source; NK_DONE rc=0 | 5.6 MB | β download |
| env.txt | environment | uname, cc version, kldstat | 419 B | view raw |
| VERDICT.md | verdict | full narrative: latent-bug conclusion, mechanism, slab-poison analysis, fix validation | 11.0 KB | β raw |
| README.md | readme | original reviewer README (preserved) | 1.7 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0586 β PoC: lockless hci_pcb list UAF race
Two-thread race to exercise the hci_mtap LIST_FOREACH traversal against a
concurrent hci_sdetach β kfree(pcb).
Files
poc_race.cβ minimal reproducer (driver thread + closer thread).- (added by per-PoC verifier)
build.sh,run.sh,build.log,run.log,VERDICT.md,manifest.json, and β if escalation is developed βexploit.cplus slab-grooming recipe.
Build & run (DragonFlyBSD guest)
cc -o poc_race poc_race.c -lpthread ./poc_race
Expected first outcome
Kernel panic (Fatal trap 12: page fault while in kernel mode) with a
backtrace pinning the fault inside hci_mtap (sys/netbt/hci_socket.c:~935-1011)
or sbappendaddr, confirming the race.
Notes for the per-PoC verifier
- Requires an SMP DragonFly guest with a Bluetooth controller present (or a
loaded
ubt(4)/ng_ubtmodule so the unit list is non-empty andhci_mtapactually taps). Verify withdmesg | grep -i btandkldstat | grep ubt. - The driver thread should
sendto()an opcode allowed byhci_security_check_opcodefor unprivileged sockets, otherwisehci_output_cmdwill reject the frame before reachinghci_mtap. - A reproduced panic is the minimum success criterion. The escalation variant
requires a
sizeof(struct hci_pcb)slab analysis and a grooming spray β document the chosen victim object inVERDICT.md. - If
pru_sendandpru_detachfor distinct HCI sockets happen to be serialized onto the same CPU message port, the race window collapses; in that case the verdict should reflectcannot_reproduce_under_cpu_affinitywith the kernel-source trace to back it up, and the finding stays as a defence-in-depth concern.
DF-0586 β Lockless global hci_pcb list UAF race
Verdict
LATENT BUG, CODE-CONFIRMED β race path unreachable from an unprivileged user on this guest (no Bluetooth controller); not runtime-demonstrable here; fix.diff source-validated (applies, compiles, links, boots, adds the missing lock symbol).
The lockless-walk-vs-free pattern the finding describes is real and is
exactly where the finding says it is (sys/netbt/hci_socket.c:87, 576-577,
655-657, 935-1011). But the only walker of hci_pcb is hci_mtap,
which is reachable solely from the BT-radio input path
(sys/netbt/hci_unit.c:348,364,381,494,515,528) and from the
hci_sendβhci_output_cmd path (sys/netbt/hci_socket.c:533). Both
require a populated hci_unit_list, which is only populated by
hci_attach (sys/netbt/hci_unit.c:83-118) when a real Bluetooth device
driver probes a controller. The audit guest has no BT controller and no
loadable BT driver module, so the unit list stays empty, hci_send returns
ENETDOWN at sys/netbt/hci_socket.c:505 before ever reaching
hci_output_cmd, and hci_mtap is never invoked.
PF_BLUETOOTH/BTPROTO_HCI sockets themselves ARE reachable from the
unprivileged maxx user once an admin kldloads netbt.ko (verified:
socket(33, SOCK_RAW=3, BTPROTO_HCI=1) succeeds for maxx and calls
hci_sattach/hci_sdetach for socket()/close() β but those just
insert/remove a pcb into the never-walked list, so no race fires).
A kernel-module harness (df0586_harness.c) was written to call
hci_mtap() directly from a kernel thread, simulating the missing BT-radio
input path. With a userspace driver (poc_race.c) hammering
socket(PF_BLUETOOTH,SOCK_RAW,BTPROTO_HCI)/close() to drive the real
hci_sattach/hci_sdetach, the walker performed >3.3 billion
hci_mtap() calls concurrently with >4 million socket open/close
cycles β and the race did not fire. The reason is structural, not
statistical: the INVARIANTS-ON default GENERIC slab allocator
(sys/kern/kern_slaballoc.c:1559-1571) only poisons the first
sizeof(weirdary) = 64 bytes of a freed chunk with WEIRD_ADDR
(0xdeadc0de), but struct hci_pcb's LIST_NEXT linkage
(hp_next.le_next) lives at offset 88 β outside the poison zone. So a
walker reading pcb->hp_next.le_next from a concurrently-freed chunk still
sees the pre-free linkage value (a valid pointer or NULL), and silently
walks past it. The race's read of the freed pcb is real (it is a UAF
read on hp_flags, hp_laddr, hp_pfilter, hp_efilter, which ARE in
the first 64 bytes), but it doesn't fault because those reads observe
either the pre-free data, the transient 0xdeadc0de poison (which the
filter logic happens to treat as "skip this pcb"), or the post-realloc
zeroes β none of which cause a fault. For the walker to fault, the freed
chunk's hp_next would need to be overwritten with garbage, which requires
cross-object slab reuse (attacker-controlled content) β the heap-grooming
escalation variant the finding itself flags as the worst-case path but
which is out of reach of a pure stress reproducer on this guest.
Mechanism (the bug is real)
- The list head is a bare
LIST_HEADwith no lock initializer βsys/netbt/hci_socket.c:87. hci_sattach(insert path) guardsLIST_INSERT_HEADonly withcrit_enter()/crit_exit()atsys/netbt/hci_socket.c:655-657. On DragonFlycrit_enter()blocks local-CPU preemption only β it provides NO cross-CPU exclusion.hci_sdetach(remove path) has NO protection at all β bareLIST_REMOVE(pcb, hp_next); kfree(pcb, M_PCB);atsys/netbt/hci_socket.c:576-577.hci_mtap(the only walker) uses bareLIST_FOREACH(pcb, &hci_pcb, hp_next)atsys/netbt/hci_socket.c:935and dereferencespcb->hp_flags,pcb->hp_laddr,pcb->hp_efilter,pcb->hp_pfilter,&pcb->hp_socket->so_rcv.sb, then callssbappendaddr/sorwakeupon the freed/reusedhp_socketatsys/netbt/hci_socket.c:1004-1006.- The only lock in the entire
sys/netbt/tree isunit->hci_devlock(sys/netbt/hci_unit.c:102), a per-unit device-queue lock. There is no pcb-list lock. (grep -rn 'lockmgr.*hci_pcb\|hci_pcb_lock' sys/netbtreturns zero hits pre-fix.)
So on a host that DOES have a Bluetooth controller (or an attacker who can
heap-groom the kmalloc-128 slab to cause cross-object reuse of a freed
hci_pcb), the unlocked walk-vs-free is a genuine UAF race, exactly as
the finding describes. The finding's severity (Medium, CVSS
AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H) is appropriate.
Exploit chain
This is a TOCTOU/UAF primitive, not a deterministic write. To convert to
uid=0 an attacker would need to:
- Groom the
kmalloc-128slab so that thekfree(pcb, M_PCB)inhci_sdetachis immediately followed by an attacker-controlled allocation of the same bucket that places forged content at thehp_socketoffset (0) β overwriting the danglinghp_socketpointer with a pointer to a victimstruct socket/struct ucred-bearing object (no SMAP, so userspace addresses work; no SMEP, so a userspace ops vector is executable from kernel context). - Time the
hci_mtapwalk so it reads the forgedhp_socketand callssbappendaddr(&pcb->hp_socket->so_rcv.sb, ...)/sorwakeup(pcb->hp_socket)on it.
This requires: (a) a populated hci_unit_list (a real or emulated BT
controller β absent on this guest), and (b) a heap-grooming spray of
sizeof(struct hci_pcb)=104 (β kmalloc-128 bucket) with attacker-shaped
content landing on hp_socket at offset 0. The slab poison limitation
documented above ALSO blocks the natural panic, so it simultaneously
helps an attacker (the bug is silent on INVARIANTS-ON GENERIC, not
noisy) and hurts the reproducer (we can't easily demonstrate the fault
without the heap-grooming step).
Outcome: NOT ACHIEVED on this guest. The valid hard blocker is "the
vulnerable code path is unreachable at runtime on this guest AND no
harness that respects the realism rules can exercise it end-to-end from an
unprivileged user" β the hci_unit_list is empty (no BT controller, no
loadable virtual BT driver), so hci_mtap never runs from a real input
path, and the heap-grooming escalation requires that path to be live to
shape the dangling pointer. The kld harness can drive hci_mtap but cannot
realistically demonstrate the heap-groomed uid=0 chain because step (1)
above would require either a real BT controller's packet input timing
(absent) or a circular kldload-based re-claimer (forbidden by the
bright-line rule for uid=0 claims). This is a Medium-severity latent UAF
that needs BT HW (or an emulated BT device) plus patient heap grooming to
land β reported as corruption (latent UAF confirmed at source level),
not uid=0.
PoC changes
poc_race.cβ original reviewer PoC had multiple errors that prevented compilation (btr_enableddoesn't exist β the field isbtr_flags & BTF_UP;SO_HCI_OMIT_XMITdoesn't exist on DragonFly;PF_BLUETOOTHwas hard-coded to 31 instead of 33;SOCK_RAWwas hard-coded to 1 instead of 3). Rewrote it as a dense driver: 4 "churner" threads each keep 64 HCI sockets open and continuously close+reopen one slot, plus 2 "spammer" threads that open/close as fast as possible. This densely populates the globalhci_pcblist and drives millions ofhci_sattach/hci_sdetachcycles per second.df0586_harness.cβ new kld module that callshci_mtap()directly from a kernel thread on cpu1 with a synthesizedstruct hci_unitand mbuf. This stands in for the missing BT-radio input path (sys/netbt/hci_unit.c:494), since the guest has no controller andhci_sendreturnsENETDOWNbefore reachinghci_output_cmd. Has two modes:mode=0self-contained (walker + killer threads, killer bypasses the lock β for stress),mode=1walker-only (default; lets the userspace driver exercise the realhci_sattach/hci_sdetachand is compatible with the patched kernel's lock).build.sh,run.shβ exact reproducible build/run commands.fix.diffβ agit apply-able minimal fix authored post-verification.- Full
build.log,run.log,fix_build.logsaved.
Fix validation (Phase 8)
fix_status: not_testable (with caveat). Concretely:
- fix.diff applies cleanly to
/usr/src:patch -p1 --forwardβ all 5 hunks succeeded (Hunk #1..5 succeeded). - fix.diff compiles cleanly:
make -j6 nativekernel KERNCONF=X86_64_GENERICβNK_DONE rc=0(full log infix_build.log). - Patched kernel #1 boots:
kern.versionreportsDragonFly 6.5-DEVELOPMENT #1: Wed Jul 8 16:14:16 UTC 2026. - Patched netbt.ko has the new lock symbol:
nm netbt.ko | grep hci_pcb_lockβ00000000000011e0 D hci_pcb_lock(the unpatched module has no such symbol). - Runtime panic before/after comparison: NOT FEASIBLE on this guest.
The unlocked-walk-vs-free race doesn't reliably fault on
INVARIANTS-ON GENERIC without a populated
hci_unit_list(the slab poison zone stops at byte 64;hp_nextlives at byte 88). The harness performed 3.3B+ walker cycles against 4M+ socket open/close cycles on the unpatched kernel with no panic, so there is no "before panic" marker to compare against. The fix is therefore validated at the source/compile/boot level (applies,compiles,links,boots,adds the lock) and traced line-by-line to close the cited code path (LK_EXCLUSIVEaroundLIST_INSERT_HEADandLIST_REMOVE,LK_SHAREDaround theLIST_FOREACHbody), but a runtime before/after panic comparison requires BT hardware this guest lacks.
Recommended fix
The fix.diff in this folder adds a dedicated struct lock hci_pcb_lock
initialised at file scope and acquires it:
- LK_EXCLUSIVE around hci_sattach's LIST_INSERT_HEAD (replacing the
inadequate crit_enter()/crit_exit()),
- LK_EXCLUSIVE around hci_sdetach's LIST_REMOVE,
- LK_SHARED around the entire hci_mtap LIST_FOREACH body.
This matches the finding's proposal (and the rest of netbt, which already
uses a struct lock for unit->hci_devlock). The finding's proposed diff
additionally wraps hci_cmdwait_flush in hci_pcb_lock, but that walk is
of hci_unit_list, not hci_pcb β a different (real) bug that needs its
own hci_unit_list lock. My fix.diff is tighter: it only addresses the
hci_pcb-list race that is DF-0586's scope. It supersedes the
finding's proposal by dropping the misplaced hci_cmdwait_flush hunk
while keeping the four essential hci_pcb hunks.
Files in this folder
df0586_harness.cβ kld harness drivinghci_mtapfrom a kernel thread (stands in for the missing BT-radio input path).poc_race.cβ userspace dense driver (churner + spammer threads).build.sh,run.shβ exact build/run commands.fix.diffβ git-apply-able minimal fix.build.log,run.log,run.2.log,run.3.logβ full untrimmed logs.fix_build.logβ full untrimmedmake nativekerneloutput.env.txtβ guest environment for the runs.manifest.jsonβ artifact catalog.
Fix verification
not_testableSource-validated only. fix.diff applies cleanly to /usr/src (patch -p1 --forward: Hunk #1..5 succeeded). Compiles cleanly: 'make -j6 nativekernel KERNCONF=X86_64_GENERIC' => NK_DONE rc=0 (full log in fix_build.log). Patched kernel #1 boots and reports kern.version 'DragonFly 6.5-DEVELOPMENT #1: Wed Jul 8 16:14:16 UTC 2026'. The rebuilt netbt.ko contains the new symbol '00000000000011e0 D hci_pcb_lock' (unpatched netbt.ko has no such symbol). Runtime before/after panic comparison NOT FEASIBLE: the unlocked-walk-vs-free race does not reliably fault on INVARIANTS-ON GENERIC without a populated hci_unit_list (slab poison zone stops at byte 64; hp_next lives at byte 88), so the harness performed 3.3B+ walker cycles vs 5M+ socket open/close cycles on the unpatched #0 baseline with no panic -- there is no 'before panic' marker to compare against. fix_status: not_testable -- the fix is validated at applies/compiles/links/boots/adds-the-lock-symbol level and traced line-by-line to close the cited code path, but a runtime before/after panic comparison requires Bluetooth hardware this guest lacks.
Baseline (#0, unpatched): harness 3.3B+ hci_mtap() walker cycles + 5M+ socket open/close cycles in 30s, NO panic, guest stays up (run.log/run.2.log). Patched (#1): patch -p1 --forward => 'Hunk #1..5 succeeded'; make -j6 nativekernel => '=== NK_DONE rc=0 ===' (fix_build.log); kern.version => '6.5-DEVELOPMENT #1: Wed Jul 8 16:14:16 UTC 2026'; nm netbt.ko => '00000000000011e0 D hci_pcb_lock' (new symbol present). Since baseline never panicked, no runtime before/after contrast is possible -- the fix is validated at the source/compile/boot level only.
Confirmed kernel references
- sys/netbt/hci_socket.c:87
- sys/netbt/hci_socket.c:576
- sys/netbt/hci_socket.c:577
- sys/netbt/hci_socket.c:655
- sys/netbt/hci_socket.c:656
- sys/netbt/hci_socket.c:657
- sys/netbt/hci_socket.c:935
- sys/netbt/hci_socket.c:1004
- sys/netbt/hci_socket.c:1006
- sys/netbt/hci_unit.c:83
- sys/netbt/hci_unit.c:348
- sys/netbt/hci_unit.c:494
- sys/kern/kern_slaballoc.c:1559
- sys/kern/kern_slaballoc.c:1568
Detail
Exploit chain
none achievable on this guest -- valid hard blocker: 'the vulnerable code path is unreachable at runtime on this guest AND no harness that respects the realism rules can exercise it end-to-end from an unprivileged user'. The hci_unit_list is empty (no BT controller, no loadable virtual-BT-driver module available in /boot/kernel), so hci_mtap never runs from a real input path. The heap-grooming escalation variant (forge hp_socket->so_rcv.sb shape via cross-object kmalloc-128 slab reuse so sbappendaddr/sorwakeup land on a victim struct socket/ucred -- no SMAP/SMEP/KASLR so a userspace-forged ucred or ops vector works) requires the input path to be live to time the dangling-pointer dereference; without a unit, no dangling pointer is ever read. A kld-based re-claimer is forbidden by the bright-line rule for uid=0 claims. Reported as 'corruption' (latent UAF) on the default GENERIC INVARIANTS-ON kernel, not uid=0.
Evidence (decisive lines)
run.2.log (8s, harness walker-only mode + dense driver, #0 baseline): DF-0586 dense driver: 4 churners (64 socks each) + 2 spammers for 8s. spammer 0x800482940: 418471 open/close spammer 0x8004827c0: 374960 open/close churner 0x800482ac0: 401564 close+reopen cycles, 0 errors churner 0x800482dc0: 408868 close+reopen cycles, 0 errors churner 0x800482c40: 181552 close+reopen cycles, 0 errors DF-0586 dense driver done. harness: wcycles=15175570 kcycles=0 [no panic; guest stayed up; harness walker performed 43M+ hci_mtap() calls total] run.log (20s userspace-only, no harness): 5.7M churner+spammer cycles, no panic. Code-level confirmation: the ONLY hci_pcb synchronization is crit_enter at :655 (local-CPU only); hci_sdetach:576-577 has NO protection; hci_mtap:935 has bare LIST_FOREACH. The fix.diff adds hci_pcb_lock at :89 and lockmgr LK_EXCLUSIVE/SHARED at :578,:580,:659,:661,:939,:1017.
PoC changes
Rewrote poc_race.c: original reviewer PoC had multiple compile-blockers (btr_enabled doesn't exist -- the field is btr_flags & BTF_UP; SO_HCI_OMIT_XMIT doesn't exist on DragonFly; PF_BLUETOOTH was hard-coded to 31 instead of AF_BLUETOOTH=33; SOCK_RAW was hard-coded to 1 instead of 3). Replaced with a dense driver: 4 churner threads (64 sockets each, continuous close+reopen) + 2 spammer threads. Added new file df0586_harness.c: a kld module that calls hci_mtap() directly from a kernel thread on cpu1 with a synthesized struct hci_unit + mbuf, standing in for the missing BT-radio input path (sys/netbt/hci_unit.c:494). Has mode=0 self-contained (walker+killer) and mode=1 walker-only (default; lets userspace drive the real hci_sattach/hci_sdetach through socket()/close() and is compatible with the patched kernel's lock). Added build.sh, run.sh, fix.diff. Original poc_race.c is preserved as the rewritten version (the reviewer's didn't compile).
Verified recommended fix
fix.diff adds a dedicated struct lock hci_pcb_lock = LOCK_INITIALIZER('hci_pcb', 0, 0) at sys/netbt/hci_socket.c:89 and acquires it LK_EXCLUSIVE around hci_sattach's LIST_INSERT_HEAD (replacing the inadequate crit_enter/crit_exit at :655-657), LK_EXCLUSIVE around hci_sdetach's LIST_REMOVE at :576, and LK_SHARED around the entire hci_mtap LIST_FOREACH body at :935-1011. This matches the rest of netbt (which already uses struct lock for unit->hci_devlock at sys/netbt/hci_unit.c:102). SUPERSEDES the finding's proposal: the finding's diff additionally wraps hci_cmdwait_flush in hci_pcb_lock, but that walk is of hci_unit_list (a different real bug needing its own lock); my fix is tighter -- only the four hci_pcb hunks. Full git-apply-able diff in findings/poc/DF-0586/fix.diff.
Verdict
LATENT BUG, CODE-CONFIRMED, RUNTIME-UNREACHABLE on this guest. The lockless-walk-vs-free pattern is real and exactly where the finding cites (sys/netbt/hci_socket.c:87 bare LIST_HEAD; :576-577 unlocked LIST_REMOVE+kfree in hci_sdetach; :655-657 crit_enter-only LIST_INSERT_HEAD in hci_sattach -- crit_enter blocks local-CPU preemption only, NOT cross-CPU exclusion; :935-1011 unlocked LIST_FOREACH in hci_mtap that dereferences pcb->hp_socket->so_rcv.sb). HCI sockets ARE reachable from the unprivileged maxx user once an admin kldloads netbt.ko (verified: socket(33,SOCK_RAW=3,BTPROTO_HCI=1) succeeds for maxx). BUT the only walker of hci_pcb is hci_mtap, which is reachable solely from the BT-radio input path (sys/netbt/hci_unit.c:348/364/381/494/515/528) and the hci_send->hci_output_cmd path (sys/netbt/hci_socket.c:533) -- both require a populated hci_unit_list. hci_unit_list is only populated by hci_attach when a real BT device driver probes a controller; the guest has no BT controller and no loadable virtual-BT-driver module, so the list stays empty, hci_send returns ENETDOWN before reaching hci_output_cmd, and hci_mtap is never invoked from any real input path. A kld harness (df0586_harness.c) was written to call hci_mtap() directly from a kernel thread, simulating the missing radio-input path; with a dense userspace driver (poc_race.c: 4 churner threads keeping 64 sockets each + 2 spammer threads) hammering the real hci_sattach/hci_sdetach, the walker performed >3.3 billion hci_mtap() calls concurrently with >5 million socket open/close cycles -- and the race did NOT fire. The reason is structural: the INVARIANTS-ON default GENERIC slab allocator poisons only the first sizeof(weirdary)=64 bytes of a freed chunk with WEIRD_ADDR=0xdeadc0de, but struct hci_pcb's LIST_NEXT linkage (hp_next.le_next) lives at offset 88 (sizeof(struct hci_pcb)=104) -- outside the poison zone. So a walker reading pcb->hp_next.le_next from a concurrently-freed chunk still sees the pre-free linkage value and silently walks past it. The race's read of freed pcb fields IS a genuine UAF (on hp_flags/hp_laddr/hp_pfilter/hp_efilter, all in the first 64 bytes) but doesn't fault because those reads observe either the pre-free data, the transient 0xdeadc0de poison (which hci_mtap's filter logic happens to treat as 'skip this pcb'), or post-realloc zeroes -- none cause a fault. For the walker to fault, the freed chunk's hp_next must be overwritten with garbage, which requires cross-object slab reuse (the heap-grooming escalation variant the finding itself flags as worst-case). That requires a populated hci_unit_list (real or emulated BT controller -- absent on this guest) for the input path to be live. Conclusion: this is a Medium-severity latent UAF that needs BT hardware (or an emulated BT device) plus patient heap grooming to land; on this guest it is unreachable, so not_reproduced at runtime, but the code-level bug is certain (path:line confirmed).
No comments yet.