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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3480 Β· 7 files
FileTypeDescriptionSize
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_cc diverging from sb_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_nextpkt store.

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 FIONREAD reported 4352 bytes queued and a fresh select()+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_testable
per-fix-DF-3480

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