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

ng_btsocket_l2cap_attach() never takes the implied pcb reference (soreference): KKASSERT panic in sonewconn on every inbound L2CAP connection; child socket refcount runs one low on production kernels (latent pcb->so UAF)

Summary

sonewconn_faddr() direct-calls pru_attach and asserts so_refs==2 ('attach + base ref'); PCB protocols take that reference when attaching. ng_btsocket_l2cap_attach() links so->so_pcb without soreference(), so every inbound connection (process_l2ca_con_ind -> sonewconn) panics INVARIANTS kernels - reproduced with a fake-lower-layer harness (panic.txt: assertion failure traced through ng_btsocket_l2cap_input on the SWI task). On non-INVARIANTS kernels the child refcount is one low: the pcb stays registered in the sockets list after the socket frees, leaving pcb->so dangling - latent UAF on accepted-socket close. Remote BT peer opening a connection to any locally listened PSM (the ordinary service-access flow) panics INVARIANTS kernels. Fix validated (soreference added): the same flow completes the whole handshake to OPEN, accept() returns, 16M+ packets transferred. Sibling attach paths (raw/rfcomm/hci_raw) flagged for their own audits.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3479 Β· 7 files
FileTypeDescriptionSize
README.md β€” 3.0 KB ↓ raw
panic.txt β€” 1.1 KB view raw
fix.diff β€” 395 B view raw
build.sh β€” 132 B view raw
run.sh β€” 187 B view raw
verdict.json β€” 3.1 KB view raw
manifest.json β€” 769 B view raw

DF-3479 β€” ng_btsocket_l2cap_attach() misses the "implied pcb ref" (soreference): KKASSERT panic in sonewconn on every inbound L2CAP connection; socket refcount runs one low on non-INVARIANTS kernels

What

DragonFly's sonewconn_faddr() (sys/kern/uipc_socket2.c) attaches a new pcb to the freshly allocated child socket by direct-calling the protocol's pru_attach and then asserts the socket reference accounting:

    so_pru_attach_direct(so, 0, &ai);
    KKASSERT(((so->so_proto->pr_flags & PR_ASYNC_RCVD) == 0 &&
        so->so_refs == 2) || ...);       /* uipc_socket2.c:398-404 */
                       /* "attach + our base ref" */

PCB-based protocols take that second reference when they attach (e.g. in_pcballoc() calls soreference(so), sys/netinet/in_pcb.c:295). ng_btsocket_l2cap_attach() (sys/netgraph7/bluetooth/socket/ng_btsocket_l2cap.c:1912-2030) links so->so_pcb/pcb->so but never calls soreference(), so on every inbound connection (ng_btsocket_l2cap_process_l2ca_con_ind() :593 -> sonewconn(pcb->so, 0)) the assertion fires on INVARIANTS kernels:

panic: assertion "... so->so_refs == 2 ..." failed in sonewconn_faddr
    at /usr/src/sys/kern/uipc_socket2.c:404
sonewconn_faddr() <- ng_btsocket_l2cap_input() <- taskqueue_run() <- ithread_handler()

On non-INVARIANTS (production) kernels the assertion is compiled out and the child socket's refcount simply runs one reference lower than the socket-layer contract β€” the pcb (still registered in ng_btsocket_l2cap_sockets) can outlive the freed socket (pcb->so dangling), a latent use-after-free whenever an accepted L2CAP socket is closed.

Threat

Remote-triggerable: any Bluetooth peer that opens a connection to a PSM a local socket is listening on (the ordinary "connect to SDP/RFCOMM service" flow) makes the kernel panic (INVARIANTS builds). Locally reproducible with the DF-3480 harness (fake ng_l2cap lower layer, no BT hardware needed): deliver NGM_L2CAP_L2CA_CON_IND for a bound/listening PSM.

Reproduce

Guest with netgraph7 bluetooth stack loaded, btsbx.ko harness (see ../DF-3480) connected to the btsock_l2c node; a listener bound to PSM 0x1234:

sysctl -w debug.btsbx_cmd=1   # setup hooks + hook_info
sysctl -w debug.btsbx_cmd=2   # con_ind -> panic (baseline)

Baseline panic.txt captured. With fix.diff applied (adds the missing soreference(so) β€” plus #include <sys/socketvar2.h>), the same command completes: the harness drove the full CON/CFG handshake to OPEN, accept() returned a child socket, and 16M+ data packets were transferred through it.

Fix (validated)

--- a/sys/netgraph7/bluetooth/socket/ng_btsocket_l2cap.c
+++ b/sys/netgraph7/bluetooth/socket/ng_btsocket_l2cap.c
@@ -1960,6 +1960,7 @@
    so->so_pcb = (caddr_t) pcb;
    pcb->so = so;
    pcb->state = NG_BTSOCKET_L2CAP_CLOSED;
+   soreference(so);    /* implied pcb ref (sonewconn expects so_refs==2) */

(note: ng_btsocket_l2cap_raw / rfcomm / hci_raw attach paths should be audited for the same omission β€” separate files.)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline: KKASSERT panic at con_ind. Patched: no panic; harness completed inbound handshake, accept() returned child socket, 16M+ packets transferred. Fix validated.

DF-3480 run.log excerpts (handshake states + t+ counters) captured on the patched module
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 (ng_btsocket.ko rebuilt with fix.diff)

Confirmed kernel references

Detail

Exploit chain

remote peer (or local graph): connect to listened PSM -> sonewconn -> attach without pcb ref -> INVARIANTS: panic; production: socket freed with pcb alive in the sockets list -> pcb->so UAF on later lookups (analysis; not runtime-demonstrated)

Evidence (decisive lines)

['panic.txt (sonewconn_faddr KKASSERT trace via ng_btsocket_l2cap_input)', 'README.md', 'fix.diff + harness completion on patched module (DF-3480 pack logs)']

PoC changes

harness-written (no seed); trigger is a single netgraph control message

Verified recommended fix

soreference(so) in ng_btsocket_l2cap_attach() after linking the pcb (mirror in_pcballoc)

Verdict

Reproduced with the btsbx.ko fake-lower-layer harness: delivering an L2CA ConnectInd for a locally listened PSM reaches ng_btsocket_l2cap_process_l2ca_con_ind -> sonewconn -> ng_btsocket_l2cap_attach, which links the pcb without taking the implied pcb reference (soreference) that sonewconn's KKASSERT (uipc_socket2.c:404) requires β€” kernel panic on INVARIANTS builds (panic.txt). On non-INVARIANTS kernels the child socket refcount runs one low; the pcb stays in ng_btsocket_l2cap_sockets after the socket is freed (pcb->so dangling) β€” latent UAF on accepted-socket close. Fix (soreference(so) after linking the pcb) validated: with the patched module the same harness completed the whole inbound handshake to OPEN, accept() returned the child, and 16M+ packets were transferred. Remote-peer triggerable on any BT system (peer connects to a listened PSM); locally triggerable via the graph.