so_rcv mutation from the SWI task without the sockbuf token: ng_btsocket_l2cap_data_input's sbappendrecord() races soreceive's ssb_token-protected unlink surgery (lost wakeups / torn record chains / potential freed-mbuf write)
Summary
data_input appends to so_rcv holding only pcb_lock/sockets_lock; sbappendrecord() is caller-serialized on DragonFly while soreceive unlinks records under so_rcv.ssb_token. No common lock - a FreeBSD-port assumption (FreeBSD's sb* are internally locked). ~2e7 appends raced against 8 readers over ~10 min produced no captured corruption, but a REPRODUCIBLE anomaly: readers slept forever after single-digit reads while FIONREAD reported 4352 queued bytes and a fresh select()+read() succeeded - the lost-wakeup/torn-tail signature (demonstrated impact: silent record loss + permanently wedged socket; analytical ceiling: write through a freed mbuf). Requires DF-3478+3479 fixed to be reachable - the pack's harness ran on the patched module. Fix: ssb_token around append+wakeup in both data_input paths (so_snd side analogous).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3480 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.8 KB | β raw | |
| run.log | β | 2.2 KB | view raw | |
| btsbx.c | β | 9.8 KB | view raw | |
| df3478_drive.c | β | 5.3 KB | view raw | |
| Makefile | β | 94 B | β download | |
| verdict.json | β | 2.5 KB | view raw | |
| manifest.json | β | 995 B | view raw |
DF-3480 β ng_btsocket_l2cap_data_input appends to so_rcv without the sockbuf token: races soreceive's unlink surgery (lost wakeups / torn record list)
What
ng_btsocket_l2cap_data_input() delivers received L2CAP packets to the
socket with:
sbappendrecord(&pcb->so->so_rcv.sb, m); /* :1455 (normal path) */
sbappendrecord(&pcb->so->so_rcv.sb, copy); /* :1529 (broadcast) */
sorwakeup(pcb->so);
holding only pcb->pcb_lock (+ ng_btsocket_l2cap_sockets_lock).
sbappendrecord() (sys/kern/uipc_sockbuf.c:176) is caller-serialized:
it reads sb->sb_mb, then writes through sb->sb_lastrecord->m_nextpkt,
then updates sb_lastrecord/sb_lastmbuf β with no internal lock and no
token taken by this caller.
The consumer β soreceive() on the user thread β performs the record
unlink surgery under so->so_rcv.ssb_token
(sys/kern/uipc_socket.c:1683/1831, sbunlinkmbuf()), which the SWI-task
appender does not hold. The two contexts share no common lock:
- append side: taskqueue_swi thread (
ng_btsocket_l2cap_inputβtaskqueue_runβithread_handlerβ the panic.txt stacks from DF-3479 prove this context). - consume side: user threads blocked in read(2).
Interleavings where the appender dereferences a sb_lastrecord that
soreceive has just unlinked (or where a wakeup is issued between the
reader's queue check and its sleep) produce:
- lost wakeups β readers sleep forever while records are queued;
- torn record chains β records linked to a detached tail,
sb_ccdiverging fromsb_mb(silent data loss / permanent socket wedge); - in the worst case a write through a freed mbuf when the unlinked
last-record is freed before the append's
m_nextpktstore.
This is a FreeBSD-port assumption: FreeBSD's sockbuf operations are internally locked (SOCKBUF_LOCK inside sb*), so the ng_btsocket code never learned to take the lock; DragonFly's sockbuf API requires the caller to serialize, and this port does not.
Runtime evidence (see run.log)
With the DF-3478 (listen recursion) and DF-3479 (missing soreference) fixes applied so the inbound path works at all, the btsbx.ko harness drove an inbound connection to OPEN and flooded it while 8 user threads read:
- 16.8M packets accepted in 240 s, zero netgraph-level drops;
- the 8 readers performed nine reads total, then slept the rest of the run;
- afterwards
FIONREADreported 4352 bytes queued and a freshselect()+read()returned another record β readers asleep while the kernel counts 25+ queued records is impossible with a correctly serialized sockbuf.
No kernel memory corruption was captured in ~10 minutes of racing, so the memory-safety ceiling (freed-mbuf write) is not claimed as reproduced; the reproduced manifestation is the lost-wakeup/wedge anomaly above.
Fix direction
Take so->so_rcv.ssb_token (lwkt token) around the append + wakeup in
ng_btsocket_l2cap_data_input() (both the normal and broadcast paths), i.e.
the same token soreceive holds during its surgery:
+ lwkt_gettoken(&pcb->so->so_rcv.ssb_token);
sbappendrecord(&pcb->so->so_rcv.sb, m);
+ lwkt_reltoken(&pcb->so->so_rcv.ssb_token);
sorwakeup(pcb->so);
(the send path's so_snd manipulation under pcb_lock only has the analogous exposure vs. any non-protocol-thread consumer of so_snd.)
Harness
btsbx.c (kernel module: fake ng_l2cap lower layer β connects to
btsock_l2c, registers a source bdaddr via NGM_L2CAP_NODE_HOOK_INFO,
drives CON_IND/CON_RSP/CFG_IND/CFG/CFG_RSP to OPEN, then floods L2CAP data
from a kthread; sysctl debug.btsbx.cmd 1=setup 2=con_ind 3=cfg_ind 4=cfg
5=flood-on 6=flood-off), df3478_drive.c (userland: listener/accept +
8 reader threads + FIONREAD/drain probes). Makefile for the module.
Requires the DF-3478 and DF-3479 fixes applied first.
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['README.md (lock analysis path:line)', 'run.log (16.8M appends, 9 reads, FIONREAD=4352 anomaly)', 'btsbx.c + df3478_drive.c + Makefile (harness)']
PoC changes
harness-written (no seed); required fixing DF-3478 and DF-3479 first to reach the vulnerable code at all
Verified recommended fix
take so_rcv.ssb_token around sbappendrecord+sorwakeup in ng_btsocket_l2cap_data_input (both paths)
Verdict
The missing-lock defect is proven by inspection: ng_btsocket_l2cap_data_input calls sbappendrecord() on so_rcv (ng_btsocket_l2cap.c:1455/:1529) from the taskqueue_swi thread holding only pcb_lock/sockets_lock, while soreceive performs record unlink surgery on the same sockbuf under so_rcv.ssb_token (uipc_socket.c:1683/1831) β no common lock. Runtime: after applying the DF-3478 and DF-3479 fixes (required to make the inbound path work at all), the btsbx harness drove an inbound connection to OPEN and raced ~2x10^7 accepted appends against 8 concurrent readers for ~10 minutes across several runs. No kernel corruption/panic was captured; however a reproducible anomaly was demonstrated that is inconsistent with a serialized sockbuf: readers performed single-digit total reads then slept forever while FIONREAD reported 4352 queued bytes and a fresh select()+read() succeeded (lost-wakeup/torn-tail signature). Filed as likely with the corruption ceiling unproven; demonstrated impact is silent data loss and a permanently wedged socket.
No comments yet.