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)
PoC verification
Evidence pack
findings/poc/DF-5740 Β· 20 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 userlandsocket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI)exercises productionhci_sattach/hci_ssend/hci_sdetach), attaches a fake controller unit via productionhci_attach()withhci_num_cmd_pkts = 0, and injects Command_Complete events through productionhci_input_event()from a kernel thread pinned to CPU 3.trigger.cβ userland: opens N promiscuous full-filter tap sockets (withSO_HCI_DIRECTIONto widen the per-copy window), then loops {open HCI socket, raise SO_SNDBUF, connect(unit), send K commands, drain taps, close}. OptionalDF5740_FEEDER=1adds a second open/send/close thread to multiply freed-socket-slot reuse.run.shβ the exact orchestration (load, setup, baseline, race).panic.txtβ the decisivepanic: receive 1backtrace + 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.
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):
-
RX side (demonstrated panic) β
hci_mtap()(hci_socket.c:1003-1010) callssbappendaddr(&so->so_rcv.sb, ...)+sorwakeup()frombtintron the queueing CPU's netisr port, holding neitherssb_locknorssb_token. A user thread insoreceive()(uipc_socket.c:1320) holdsssb_token+ssb_lock, which do not exclude the netisr appender. Torn interleavings desynchronizessb_mbfromssb_cc. Reproduced: INVARIANTS kernel panicked in 24 s withpanic: receive 1(uipc_socket.c:1348:KASSERT(m != NULL || !ssb_cc)) βsoreceive()+0xd1fviasys_recvfrom, cpu4, while cpu3 ran the completion flood (panic.txt). On production kernels the same interleave silently corrupts (see 3). -
TX side, live socket β
hci_send()appends the "precious" record withsbappendrecordon cpu0's port (hci_socket.c:522) whilehci_drop()drops it withsbdroprecord/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.logshows the damage accumulating as non-EWOULDBLOCK send failures (96 by iter 875) on the racedso_snd. -
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 withhci_cmdwait_flush()(hci_socket.c:446-465), but that sweep only sees mbufs still inunit->hci_cmdwait: any command mbuf already dequeued by the concurrent completion path (hci_event.c:282/354 βhci_output_cmdhci_unit.c:490-509, which reads the ctx at :505 after a longhci_mtapwalk over every listening socket) is invisible to it. After the sweep, detach setsso_pcb = NULL,sofree()(hci_socket.c:573-574) drops the last reference βssb_releaseβsbflushfrees the pending so_snd records andsodeallocfrees the socket (uipc_socket.c:380-437). The completion cpu then executeshci_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 slotssbflushhas already NULLedsb_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
fixedfix.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)
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.
No comments yet.