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)
PoC verification
Evidence pack
findings/poc/DF-3479 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
fixedBaseline: 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
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.
No comments yet.