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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3478 Β· 9 files
FileTypeDescriptionSize
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.ko to 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.

VERDICT.md
↓ download raw

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():

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline 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'
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (ng_btsocket.ko module rebuilt with fix.diff, same base kernel)

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