β¬’ DragonFlyBSD Kernel Audit
← triage Β· 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).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0586 Β· 14 files
FileTypeDescriptionSize
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
README.md readme original reviewer README (preserved)
↓ download 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.c plus 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_ubt module so the unit list is non-empty and hci_mtap actually taps). Verify with dmesg | grep -i bt and kldstat | grep ubt.
  • The driver thread should sendto() an opcode allowed by hci_security_check_opcode for unprivileged sockets, otherwise hci_output_cmd will reject the frame before reaching hci_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 in VERDICT.md.
  • If pru_send and pru_detach for distinct HCI sockets happen to be serialized onto the same CPU message port, the race window collapses; in that case the verdict should reflect cannot_reproduce_under_cpu_affinity with the kernel-source trace to back it up, and the finding stays as a defence-in-depth concern.
VERDICT.md verdict full narrative: latent-bug conclusion, mechanism, slab-poison analysis, fix validation
↓ download raw

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)

  1. The list head is a bare LIST_HEAD with no lock initializer β€” sys/netbt/hci_socket.c:87.
  2. hci_sattach (insert path) guards LIST_INSERT_HEAD only with crit_enter()/crit_exit() at sys/netbt/hci_socket.c:655-657. On DragonFly crit_enter() blocks local-CPU preemption only β€” it provides NO cross-CPU exclusion.
  3. hci_sdetach (remove path) has NO protection at all β€” bare LIST_REMOVE(pcb, hp_next); kfree(pcb, M_PCB); at sys/netbt/hci_socket.c:576-577.
  4. hci_mtap (the only walker) uses bare LIST_FOREACH(pcb, &hci_pcb, hp_next) at sys/netbt/hci_socket.c:935 and dereferences pcb->hp_flags, pcb->hp_laddr, pcb->hp_efilter, pcb->hp_pfilter, &pcb->hp_socket->so_rcv.sb, then calls sbappendaddr/sorwakeup on the freed/reused hp_socket at sys/netbt/hci_socket.c:1004-1006.
  5. The only lock in the entire sys/netbt/ tree is unit->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/netbt returns 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:

  1. Groom the kmalloc-128 slab so that the kfree(pcb, M_PCB) in hci_sdetach is immediately followed by an attacker-controlled allocation of the same bucket that places forged content at the hp_socket offset (0) β€” overwriting the dangling hp_socket pointer with a pointer to a victim struct socket/struct ucred-bearing object (no SMAP, so userspace addresses work; no SMEP, so a userspace ops vector is executable from kernel context).
  2. Time the hci_mtap walk so it reads the forged hp_socket and calls sbappendaddr(&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_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 33; SOCK_RAW was 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 global hci_pcb list and drives millions of hci_sattach/hci_sdetach cycles per second.
  • df0586_harness.c β€” new kld module that calls hci_mtap() directly from a kernel thread on cpu1 with a synthesized struct hci_unit and mbuf. This stands in for the missing BT-radio input path (sys/netbt/hci_unit.c:494), since the guest has no controller and hci_send returns ENETDOWN before reaching hci_output_cmd. Has two modes: mode=0 self-contained (walker + killer threads, killer bypasses the lock β€” for stress), mode=1 walker-only (default; lets the userspace driver exercise the real hci_sattach/hci_sdetach and is compatible with the patched kernel's lock).
  • build.sh, run.sh β€” exact reproducible build/run commands.
  • fix.diff β€” a git apply-able minimal fix authored post-verification.
  • Full build.log, run.log, fix_build.log saved.

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 in fix_build.log).
  • Patched kernel #1 boots: kern.version reports DragonFly 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_next lives 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_EXCLUSIVE around LIST_INSERT_HEAD and LIST_REMOVE, LK_SHARED around the LIST_FOREACH body), but a runtime before/after panic comparison requires BT hardware this guest lacks.

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 driving hci_mtap from 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 untrimmed make nativekernel output.
  • env.txt β€” guest environment for the runs.
  • manifest.json β€” artifact catalog.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Source-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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Wed Jul 8 16:14:16 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (single-fix kernel built from with-src baseline + fix.diff; netbt.ko rebuilt and confirmed to contain hci_pcb_lock at 00000000000011e0 D)

Confirmed kernel references

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