listen(2) on an L2CAP SEQPACKET socket recurses solisten()βso_pru_listen() without bound β kernel stack overflow β CPU double fault (unprivileged, 100% reproducible panic)
Summary
The pru_listen handler ng_btsocket_l2cap_listen() calls DragonFly's solisten(), but solisten() itself ends with so_pru_listen() which re-dispatches the same netmsg inline on the same netisr cpu0 thread. Unbounded recursion exhausts the kernel stack; the return-address push for lwkt_domsg inside so_pru_listen hits an unmapped page -> 'panic: double fault' -> DDB, dead until reboot. FreeBSD porting defect (FreeBSD's solisten does not re-dispatch). REPRODUCED 3/3: three syscalls from any user, no Bluetooth hardware needed: socket+bind+listen -> serial console 'STEP: bind done' then 'DOUBLE FAULT / panic: double fault', rip = so_pru_listen+0x4d. Also bricks the entire Bluetooth subsystem (no listener can ever be created). Fix validated in-guest (delete the solisten() call): patched module prints 'ALL OK' and the listener accepts real connections (16M+ packets in the follow-on harness). Unpriv local whenever ng_btsocket.ko is loaded. RFCOMM twin at ng_btsocket_rfcomm.c:974/:1427 flagged for that file's audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3478 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.7 KB | β raw | |
| VERDICT.md | β | 3.5 KB | β raw | |
| step_listen.c | β | 1.2 KB | view raw | |
| build.sh | β | 96 B | view raw | |
| run.sh | β | 162 B | view raw | |
| panic.txt | β | 1.0 KB | view raw | |
| fix.diff | β | 1009 B | view raw | |
| verdict.json | β | 2.9 KB | view raw | |
| manifest.json | β | 716 B | view raw |
DF-3478 β listen(2) on L2CAP SEQPACKET socket: unbounded solisten()βpru_listen recursion β kernel stack overflow β double fault (unprivileged kernel panic)
What
ng_btsocket_l2cap_listen() β the pru_listen handler for
PF_BLUETOOTH/SOCK_SEQPACKET/L2CAP β ends with:
solisten(so, backlog, td); /* ng_btsocket_l2cap.c:2460 */
On DragonFly, solisten() (sys/kern/uipc_socket.c:330-344) is the syscall-side
helper: it sets SO_ACCEPTCONN/so_qlimit and then calls
so_pru_listen(so, td) itself (uipc_socket.c:343), which dispatches the
listen netmsg to so->so_port β for this socket family
netisr_cpuport(0) (no pr_initport in the protosw, sys/kern/uipc_socket.c:259).
The handler therefore re-enters its own dispatch:
listen(2) β kern_listen β solisten β so_pru_listen
β lwkt_domsg(netisr_cpuport(0)) β ng_btsocket_l2cap_listen
β solisten β so_pru_listen
β lwkt_domsg(same port; dispatched inline on the netisr thread)
β ng_btsocket_l2cap_listen β solisten β ... (unbounded)
Each level consumes kernel stack until the push in so_pru_listen+0x4d
(the callq lwkt_domsg) lands on an unmapped page β CPU double fault β
panic: double fault β system down in DDB.
This is a porting defect: in FreeBSD solisten() does not re-dispatch
pru_listen; the 2008-era netgraph7 port kept the call without adapting it to
DragonFly's message-dispatching socket layer.
Reachability / threat
- Requires only
ng_btsocket.koto be loaded (Bluetooth stack in use). socket()+bind()+listen()β no privilege needed anywhere (see DF-3477 for the unprivileged bind). Three syscalls, 100% reproducible.- No Bluetooth hardware or peer required.
Reproduce (3/3 reproductions, two independent boots + one fresh driver)
cc -o step_listen step_listen.c ./step_listen
Console (serial) shows, then the guest dies:
STEP: start STEP: socket done STEP: bind done DOUBLE FAULT Fatal double fault rip = 0xffffffff806c2b4d <-- so_pru_listen+0x4d (callq lwkt_domsg) rsp = 0xfffff8008d1f6000 panic: double fault
(0xffffffff806c2b4d resolves inside so_pru_listen in the guest's
/boot/kernel/kernel via nm/objdump.)
Fix (validated)
Remove the solisten() call from the handler β the syscall side already ran
it before dispatch. See fix.diff. With the patched module the same program
prints:
STEP: listen done STEP: ALL OK
and accept() on the listener works (used by the DF-3479/DF-3480 harness to
drive an inbound connection to OPEN).
The same defect exists in ng_btsocket_rfcomm.c:974 (and :1427 indirectly)
β separate file, reported via cross-reference.
DF-3478 β VERDICT
status: reproduced / impact: panic (permanent, unprivileged local DoS) / confidence: certain
How it was reproduced
Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), netgraph7 module stack loaded (netgraph.ko, ng_bluetooth.ko, ng_btsocket.ko).
step_listen (see step_listen.c) performs exactly three operations:
socket(PF_BLUETOOTH=33, SOCK_SEQPACKET, BLUETOOTH_PROTO_L2CAP=135);
bind(..., {psm=0x1234, bdaddr=ANY});
listen(fd, 4);
Serial console (serial console writes survive the panic):
STEP: start STEP: socket done STEP: bind done DOUBLE FAULT Fatal double fault rip = 0xffffffff806c2b4d panic: double fault
3/3 reproductions across two independent boots (the third occurrence was the
DF-3480 harness driver, which panics identically at its listen() call).
uid=1001 can perform all three syscalls (DF-3477 shows bind needs no
privilege; socket/listen never check privilege either) β the panic is
unprivileged whenever the bluetooth socket module is loaded.
Root cause (line-accurate)
ng_btsocket_l2cap_listen() β the pru_listen netmsg handler executed on the
netisr cpu0 protocol thread β calls DragonFly's solisten():
- sys/netgraph7/bluetooth/socket/ng_btsocket_l2cap.c:2460 β
solisten(so, backlog, td); - sys/kern/uipc_socket.c:330-344 β
solisten()sets SO_ACCEPTCONN/qlimit and ends withreturn so_pru_listen(so, td);(:343) - sys/kern/uipc_msg.c:341-352 β
so_pru_listen()doeslwkt_domsg(so->so_port, ...); for this familyso_port = netisr_cpuport(0)(nopr_initportin the protosw; sys/kern/uipc_socket.c:259).
So the handler re-dispatches itself; the domsg is executed inline on the same protocol thread, recursing without bound:
kern_listen -> solisten -> so_pru_listen -> [netisr0] ng_btsocket_l2cap_listen -> solisten -> so_pru_listen -> [inline] ng_btsocket_l2cap_listen -> ...
until the kernel stack overflows; the deepest frame is always the
callq lwkt_domsg inside so_pru_listen (rip 0xffffffff806c2b4d =
so_pru_listen+0x4d, verified against the guest's /boot/kernel/kernel),
whose return-address push lands on an unmapped page β CPU double fault β
panic: double fault β DDB, system dead until reboot.
This is a porting defect: FreeBSD's solisten() does not re-dispatch
pru_listen, so the 2008-era ng7 port kept a call that is recursively
self-invoking under DragonFly's message-based socket layer.
Fix validation
fix.diff removes the solisten() call (and the now-unused td/backlog
variables). Applied to the guest's /usr/src copy, ng_btsocket.ko rebuilt and
reloaded:
STEP: start STEP: socket done STEP: bind done STEP: listen done STEP: ALL OK (RC=0)
and the listener thereafter accepts real connections (used by the DF-3479 / DF-3480 harness, which drives an inbound L2CAP connection to OPEN and transfers 16M+ packets). Baseline panics; patched completes β fix validated.
Cross-references
- Same defect in the RFCOMM twin: sys/netgraph7/bluetooth/socket/ ng_btsocket_rfcomm.c:974 (its own listener) and :1427 (session's L2CAP listener β reaches the l2cap handler's recursion from the rfcomm task thread). Different file; flagged for that file's audit.
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
fixedBaseline module: double fault at listen() (3/3). Patched module (fix.diff): listen() completes, 'ALL OK', listener accepts inbound L2CAP connections driven by the DF-3479/3480 harness (handshake to OPEN, accept(), 16M+ data packets). Bad behavior gone.
VERDICT.md 'Fix validation' section; patched run output 'STEP: listen done / STEP: ALL OK'
Confirmed kernel references
Detail
Exploit chain
unpriv user: socket()->bind()->listen() -> kernel stack overflow -> double fault panic (system-wide DoS). No uid=0 route β pure availability impact.
Evidence (decisive lines)
['panic.txt (serial console: STEP markers then DOUBLE FAULT, rip=so_pru_listen+0x4d)', 'VERDICT.md (full trace)', 'step_listen.c', "fix.diff + patched-run 'STEP: ALL OK'"]
PoC changes
harness-written (no seed); step_listen marks progress on /dev/console so the last completed syscall is visible after the panic
Verified recommended fix
remove the solisten() call from ng_btsocket_l2cap_listen() (syscall side already ran it before dispatch)
Verdict
Reproduced 3/3: socket(PF_BLUETOOTH,SOCK_SEQPACKET,L2CAP)+bind+listen() double-faults the kernel. ng_btsocket_l2cap_listen() (the pru_listen netmsg handler on the netisr cpu0 thread) calls DragonFly's solisten(), which itself ends with so_pru_listen() (uipc_socket.c:343) re-dispatching the same netmsg inline on the same thread β unbounded recursion exhausts the kernel stack; the return-address push for lwkt_domsg inside so_pru_listen (rip so_pru_listen+0x4d) hits an unmapped page and the CPU takes a double fault. Unprivileged (no priv check on socket/bind/listen for this family), no Bluetooth hardware needed, 100% reliable, permanent DoS until reboot. Fix (delete the solisten() call) validated on the guest: baseline panics, patched prints 'ALL OK' and the listener then accepts connections (16M+ packets transferred through it in the follow-on harness).
No comments yet.