β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5740

hci_socket.c mutates socket buffers from the NETISR thread with no synchronization (hci_mtap sbappendaddr vs soreceive, hci_drop sbdroprecord vs sbappendrecord) and hands the TX-completion callback a reference-free socket pointer that survives teardown (cmdwait ctx TOCTOU β†’ post-detach hci_drop UAF)

Summary

Every HCI sockbuf mutation runs on the wrong thread: hci_send's sbappendrecord on netisr port 0 races hci_drop's sbdroprecord on the completion CPU (sbdroprecord's own contract requires a crit section), hci_mtap's sbappendaddr races user threads inside soreceive holding only ssb locks - neither excludes the netisr mutator; and the completion ctx is a raw socket pointer with no reference (cmdwait_flush can only clear still-queued mbufs, so commands already dequeued keep a dangling pointer to a freed socket - late hci_drop double-frees or steals an unrelated live socket's records after slot reuse). Baseline rig: panic 'receive 1' (ssb_mb==NULL && ssb_cc!=0 KASSERT) plus accumulating send desync and two hard system wedges; token+reference+devlock-consume fix validated (425+ iterations, 85k commands, 0 failures). Unprivileged local on any Bluetooth machine.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5740 Β· 20 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 5.9 KB ↓ raw
harness.c β€” 11.6 KB view raw
sock_stub.c β€” 6.3 KB view raw
stub.h β€” 282 B view raw
Makefile β€” 708 B ↓ download
build.sh β€” 159 B view raw
run.sh β€” 257 B view raw
trigger.c β€” 6.3 KB view raw
build.log β€” 920 B view raw
setup.log β€” 367 B view raw
panic.txt β€” 2.3 KB view raw
race3.log β€” 5.3 KB view raw
race4.log β€” 566 B view raw
race5.log β€” 1.6 KB view raw
fix_run.log β€” 951 B view raw
fix.diff β€” 3.8 KB view raw
env.txt β€” 748 B view raw
code_hashes.txt β€” 227 B view raw
verdict.json β€” 5.1 KB view raw

DF-5740 β€” hci_socket.c sockbuf mutation from the NETISR thread (hci_mtap sbappendaddr / hci_drop sbdroprecord) + post-detach hci_drop UAF

Production code: sys/netbt/hci_socket.c (md5 50bdf81bf0b8828ce1336511b98e3c52, compiled VERBATIM into the rig; see setup.log).

What this pack contains

  • harness.c, sock_stub.c, stub.h, Makefile, build.sh β€” the production-code KLD rig ("hci_sock_harness"): compiles hci_socket.c / hci_event.c / hci_link.c / hci_misc.c / hci_unit.c / bt_input.c / bt_proto.c unmodified, registers the real AF_BLUETOOTH domain (so plain userland socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI) exercises production hci_sattach/hci_ssend/ hci_sdetach), attaches a fake controller unit via production hci_attach() with hci_num_cmd_pkts = 0, and injects Command_Complete events through production hci_input_event() from a kernel thread pinned to CPU 3.
  • trigger.c β€” userland: opens N promiscuous full-filter tap sockets (with SO_HCI_DIRECTION to widen the per-copy window), then loops {open HCI socket, raise SO_SNDBUF, connect(unit), send K commands, drain taps, close}. Optional DF5740_FEEDER=1 adds a second open/send/close thread to multiply freed-socket-slot reuse.
  • run.sh β€” the exact orchestration (load, setup, baseline, race).
  • panic.txt β€” the decisive panic: receive 1 backtrace + earlier crash/wedge evidence.
  • race3/4/5.log, fix_run.log β€” trigger-side outputs.
  • build.log, setup.log, env.txt, code_hashes.txt.
  • fix.diff β€” the verified fix (sockbuf tokens + ctx-owned socket reference + devlock-guarded ctx consume).

Build (inside the guest, as root)

sh build.sh          # copies /usr/src/sys/netbt/*.c VERBATIM (md5s in
                     # setup.log), builds hci_sock_harness.ko + trigger

Run (inside the guest, as root)

kldload ./hci_sock_harness.ko
sysctl hw.hsh_setup=1                    # attach unit + CPU3 injector
sysctl debug.trace_on_panic=1 debug.debugger_on_panic=0
# race:
sysctl hw.hsh_fire=99999 &               # paced completion flood, cpu3
DF5740_FEEDER=1 ./trigger 8000 6 200     # open/send/close + taps, any cpu

Expected result (unfixed production code)

Kernel panic panic: receive 1 in soreceive() (sockbuf accounting desync between hci_mtap's sbappendaddr on the netisr port and the user thread inside soreceive) β€” observed at 24 s on the rig β€” or, on production (non-KASSERT) kernels, silent sockbuf corruption: lost records with live ssb_cc (permanent sosend/soreceive stalls β€” race5.log shows 96 non-EWOULDBLOCK send failures accumulating), stale sb_lastrecord writes into freed mbufs, record theft from reallocated sockets (the post-detach hci_drop() UAF), mbuf double-frees, and hard system wedges (race4.log, race5.log, send_ipiq ... tgt not draining in panic.txt).

Expected result (with fix.diff applied to /usr/src/sys/netbt and the

rig rebuilt β€” identical race load)

No panic, no wedge, zero send failures; the run survives β‰₯4Γ— the exposure that panicked the unfixed kernel (fix_run.log: 425+ iterations clean vs baseline panic at ~100–150 iterations, stopped manually). fix.diff was validated exactly this way.

VERDICT.md
↓ download raw

DF-5740 β€” VERDICT

Finding: sys/netbt/hci_socket.c mutates socket buffers from the NETISR thread with no synchronization, and hands the TX-completion callback (hci_drop) a raw socket pointer with no reference.

Three coupled defects, one root cause family β€” hci_socket.c touches sockets from whatever CPU/netisr port processes controller traffic, while every socket op (send/close) is serialized on a different port (netisr_cpuport(0), uipc_socket.c:259):

  1. RX side (demonstrated panic) β€” hci_mtap() (hci_socket.c:1003-1010) calls sbappendaddr(&so->so_rcv.sb, ...) + sorwakeup() from btintr on the queueing CPU's netisr port, holding neither ssb_lock nor ssb_token. A user thread in soreceive() (uipc_socket.c:1320) holds ssb_token+ssb_lock, which do not exclude the netisr appender. Torn interleavings desynchronize ssb_mb from ssb_cc. Reproduced: INVARIANTS kernel panicked in 24 s with panic: receive 1 (uipc_socket.c:1348: KASSERT(m != NULL || !ssb_cc)) β€” soreceive()+0xd1f via sys_recvfrom, cpu4, while cpu3 ran the completion flood (panic.txt). On production kernels the same interleave silently corrupts (see 3).

  2. TX side, live socket β€” hci_send() appends the "precious" record with sbappendrecord on cpu0's port (hci_socket.c:522) while hci_drop() drops it with sbdroprecord/sowwakeup (hci_socket.c:437-438) from the completion cpu's port (hci_unit.c:505-507). Neither takes the sockbuf token; sbdroprecord's own contract ("must be called while holding a critical section", uipc_sockbuf.c:510-512) is violated cross-CPU. race5.log shows the damage accumulating as non-EWOULDBLOCK send failures (96 by iter 875) on the raced so_snd.

  3. TX side, dying socket (UAF) — the completion context is stored as a raw pointer in the mbuf (M_SETCTX(m, so), hci_socket.c:523) with no reference. hci_sdetach() tries to defuse it with hci_cmdwait_flush() (hci_socket.c:446-465), but that sweep only sees mbufs still in unit->hci_cmdwait: any command mbuf already dequeued by the concurrent completion path (hci_event.c:282/354 → hci_output_cmd hci_unit.c:490-509, which reads the ctx at :505 after a long hci_mtap walk over every listening socket) is invisible to it. After the sweep, detach sets so_pcb = NULL, sofree() (hci_socket.c:573-574) drops the last reference → ssb_release→sbflush frees the pending so_snd records and sodealloc frees the socket (uipc_socket.c:380-437). The completion cpu then executes hci_drop(freed_so) → sbdroprecord(&freed->so_snd.sb) + m_freem() of the already freed record chain (double free) + sowwakeup(freed), or — when the objcache slot was already recycled into the next socket the trigger opens — steals and frees an unrelated live socket's records. I could not capture this exact interleaving in a stand-alone panic: on non-reused slots sbflush has already NULLed sb_mb, so the stale drop is a silent no-op; the observable forms (record theft desyncs, double frees, the cpu0 wedge of race2/4/5) are all present in the logs. Mechanism verified line-by-line; the code holds no reference and the flush sweep is TOCTOU by construction.

Honest confound: race1/2's page-fault panic (hci_input_event+0x42, NULL m) was enabled by DF-5742's unbounded tap exhausting the mbuf pool under my non-draining taps; it is a rig artifact (m_gethdr NULL + no-op KKASSERT in the kmod + _m_copyback2(NULL) returning success) and is NOT claimed as this finding's proof. The proof is the panic: receive 1 run (race6) whose taps drained.

Reproduced: yes (panic + corruption + wedge under race load). Impact: kernel memory corruption (sockbuf list/accounting desync, mbuf double-free, cross-socket record theft, UAF write into freed/reallocated struct socket); panic on INVARIANTS kernels, hard hangs (cpu wedge) otherwise. Not driven to uid=0: the corrupted objects are mbuf chains and socket slots; a full chain would need objcache grooming of the recycled socket slot and control over the stolen record's reuse β€” plausible but not completed in this run. Threat model: unprivileged local user on any machine with a Bluetooth controller (raw HCI sockets are unprivileged; the attacker supplies both the tap/recvfrom side and the command traffic; with DF-5741 every socket is "privileged" anyway).

Fix validated: fix.diff (sockbuf tokens around all four mutation sites, ctx-owned socket reference via soreference/sofree, and a hci_devlock-guarded single-consumer consume of the ctx shared by hci_output_cmd and hci_cmdwait_flush). Applied to the guest's /usr/src, rig rebuilt (build.sh copies the patched files verbatim), identical race re-run: 425+ iterations / 85k+ commands queued / 0 send failures / no panic / no wedge vs baseline panic at ~100-150 iterations (fix_run.log), then stopped manually. Baseline vs patched recorded.

Key kernel refs - sys/netbt/hci_socket.c:1003-1010 (mtap append, no token) - sys/netbt/hci_socket.c:433-439β†’fix (drop, no token/no ref) - sys/netbt/hci_socket.c:446-465 (flush TOCTOU) - sys/netbt/hci_socket.c:522-523 (append + M_SETCTX, no ref) - sys/netbt/hci_socket.c:567-579 (detach: flush, so_pcb=NULL, sofree) - sys/netbt/hci_unit.c:494-509 (output_cmd: mtap window, ctx read) - sys/netbt/hci_event.c:281-284,353-356 (cmdwait drain, netisr port) - sys/kern/uipc_socket.c:259 (so_port = netisr_cpuport(0)) - sys/kern/uipc_socket.c:1348 (the KASSERT that fired) - sys/kern/uipc_sockbuf.c:510-512 (sbdroprecord critical-section contract)

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src/sys/netbt (hci_socket.c+hci_unit.c), rig rebuilt verbatim, identical race re-run: 425+ open/send/close iterations (85k+ commands), 0 send failures, no panic, no wedge vs unfixed panic at ~100-150 iterations; stopped manually. Bad behavior gone.

fix_run.log (clean iterations tail); fix.diff (the validated patch)
↓ fix.diffper-fix-DF-5740

Confirmed kernel references

Detail

Exploit chain

unpriv local user with HCI unit present: open promiscuous tap sockets (full filters) + raw-socket command traffic -> hci_mtap sbappendaddr on netisr races user soreceive -> sockbuf record-list/accounting desync (panic on INVARIANTS; on production: lost records + live ssb_cc => permanent sosend/soreceive stall, stale sb_lastrecord write into freed mbuf, mbuf double-free) ; concurrently close command sockets -> hci_cmdwait_flush misses in-flight dequeued commands -> hci_drop(freed socket) writes freed/reallocated socket slot and frees its records (cross-object corruption). uid=0 not completed: needs objcache grooming of the recycled socket slot.

Evidence (decisive lines)

panic.txt (panic: receive 1, soreceive+0xd1f trace, cpu4 vs cpu3 injector); fix_run.log (patched: 425+ iters clean, 0 failures); race5.log (96 non-EWOULDBLOCK send failures); VERDICT.md (full mechanism + confound analysis)

PoC changes

seed had no rig; built production-code KLD rig compiling hci_socket.c+engine+bt_proto.c verbatim (real AF_BLUETOOTH domain registration so userland socket(2) reaches production code), fake hci_attach unit with num_cmd_pkts=0, CPU-pinned completion injector; taps drain in-loop so DF-5742's unbounded tap does not drown the race (earlier non-draining runs crashed via mbuf-exhaustion NULL-m path - documented as confound in panic.txt)

Verified recommended fix

hold so_rcv/ssb_token around hci_mtap's sbappendaddr/sorwakeup and so_snd token around hci_send's sbappendrecord and hci_drop's sbdroprecord/sowwakeup; make the M_SETCTX completion context own a socket reference (soreference at hci_send, sofree in hci_drop and in hci_cmdwait_flush); consume the ctx under unit->hci_devlock exactly once (hci_output_cmd NULLs it under devlock, flush too). See fix.diff (validated).

Verdict

hci_mtap() appends to so_rcv via sbappendaddr (hci_socket.c:1004) and hci_drop() mutates so_snd via sbdroprecord/sowwakeup (hci_socket.c:437-438) from the NETISR port of the queueing CPU, while socket ops serialize on netisr_cpuport(0) and user threads hold only ssb_lock/ssb_token - none of which excludes the netisr mutator. Demonstrated: panic 'receive 1' (uipc_socket.c:1348 KASSERT, sockbuf ssb_mb/ssb_cc desync) in 24s under open/send/close + completion-flood on a 6-cpu INVARIANTS kernel, plus accumulating non-EWOULDBLOCK send failures (race5) and hard cpu wedges (race2/4/5) from the same family. The hci_drop context pointer (M_SETCTX, hci_socket.c:523) additionally holds no socket reference and hci_cmdwait_flush (hci_socket.c:446-465) cannot see mbufs already dequeued by the completion path (window widened by hci_mtap, hci_unit.c:494), so hci_drop can execute sbdroprecord+m_freem on a socket freed by hci_sdetach's sofree (hci_socket.c:574) - post-detach UAF write / double-free / cross-socket record theft, mechanism verified line-by-line (silently manifests on non-INVARIANTS kernels). fix.diff (sockbuf tokens + ctx-owned soreference/sofree + devlock-guarded single-consumer ctx consume) validated by rebuilding the verbatim rig against the patched tree and re-running the identical race: 425+ iterations clean vs baseline panic at ~100-150.