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

v1 ng_socket: ngc_send/ng_connect_cntl deref pcbp->sockdata with no NULL check after node shutdown β€” deterministic kernel panic via connect() on a still-open control socket

Summary

ngc_send:257 and ng_connect_cntl:694/701 lack the sockdata==NULL check their sibling paths have (ngd_send:357, ng_bind:714, ng_setsockaddr:444). After the node is shut down - deterministically by sendto(NGM_SHUTDOWN) addressed to the socket's own node (ngs_rmnode NULLs pcbp->sockdata, kfrees the ngsock and calls soisconnected while leaving the socket open with so_pcb=pcbp) - connect() on that socket reaches ngc_connect because soconnect has no SS_ISDISCONNECTED gate. REPRODUCED on guest: Fatal trap 12, fault vaddr 0x0, 'Stopped at ngc_connect+0x52: movq (%r13),%rdi' - 4 syscalls, 100% reliable (root-held control socket; NGM_SHUTDOWN from any control socket, NOLINGER disconnect, or cross-node rmnode; realistic: a root netgraph daemon reconnecting after its graph was removed). DoS only. Fix validated (two ENOTCONN guards, module rebuild): panic gone, clean ENOTCONN, good path unaffected.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3516 Β· 10 files
FileTypeDescriptionSize
df3516.c β€” 3.4 KB view raw
build.sh β€” 138 B view raw
run.sh β€” 102 B view raw
run.log β€” 1.4 KB view raw
run.fix.log β€” 584 B view raw
panic.txt β€” 1.2 KB view raw
env.txt β€” 301 B view raw
fix.diff β€” 645 B view raw
VERDICT.md β€” 4.5 KB ↓ raw
verdict.json β€” 3.1 KB view raw
VERDICT.md
↓ download raw

DF-3516 VERDICT β€” REPRODUCED (panic), fix validated

Reproduced? YES β€” deterministic kernel panic, 4 syscalls, no race.

Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, stock INVARIANTS kernel), netgraph.ko + ng_socket.ko from /boot/kernel. PoC run as root.

The chain, line by line

  1. socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL=2) β†’ ngc_attach (sys/netgraph/socket/ng_socket.c:165-182). Root-gated at :172 (caps_priv_check(SYSCAP_RESTRICTEDROOT|__SYSCAP_NULLCRED)) β†’ ng_attach_cntl (:476-503): creates pcb + ngsock, pcbp->sockdata = privdata, sockdata->refs = 1.
  2. bind(s, {"shutme"}) β†’ ngc_bind β†’ ng_bind (:709-723): names the node.
  3. sendto(s, NGM_SHUTDOWN, ..., "shutme:") β†’ ngc_send (:201-273): pcbp->sockdata->node (:257) is still valid here; ng_send_msg (sys/netgraph/netgraph/ng_base.c:1208-1233) resolves the path and runs ng_generic_msg β†’ case NGM_SHUTDOWN: ng_rmnode(here) (ng_base.c:1256-1258). ng_rmnode (:425-449) sets NG_INVALID and calls the type shutdown ngs_rmnode (ng_socket.c:910-935): - dpcbp/pcbp non-NULL β†’ soisdisconnected(pcbp->ng_socket), pcbp->sockdata = NULL, sockdata->refs-- - node->private = NULL, ng_unref(node), kfree(sockdata) β€” the control socket stays open with so_pcb = pcbp (nothing clears it; only ngc_detach does). sendto returns 52 (bytes sent).
  4. connect(s, {"shutme:link0"}) β†’ soconnect (sys/kern/uipc_socket.c:685-713): no SS_ISCONNECTED/SS_ISDISCONNECTED rejection for PR_ATOMIC|PR_ADDR sockets β†’ so_pru_connect β†’ ngc_connect (:291-303) β†’ ng_connect_cntl (:680-703): sockdata = pcbp->sockdata == NULL; ng_path_parse("shutme:link0") returns node+hook (both non-NULL, so the :690 guard passes); :694 ng_findname(sockdata->node, node) evaluates sockdata->node β€” struct ngsock.node is the first member (offset 0) β†’ read of vaddr 0x0.

Observed (dfbsd-qemu/boot.log, captured in panic.txt / run.log):

Fatal trap 12: page fault while in kernel mode
fault virtual address    = 0x0
fault code               = supervisor read data, page not present
instruction pointer      = 0x8:0xffffffff8263e9d2
Stopped at      ngc_connect+0x52:       movq    (%r13),%rdi

movq (%r13),%rdi is exactly the load of sockdata->node (r13 = NULL sockdata) whose result feeds ng_findname's first argument. Guest died; ssh hung; vm status "down" right after. Classification: reproduced / impact=panic.

Why not uid0?

NULL-deref read at a fixed offset 0 β€” a denial-of-service primitive, not a write. No escalation path.

PoC corrections vs the seed I wrote myself

  • NG_CONTROL is 2 on DragonFly (guessed 1) β€” use <netgraph/socket/ng_socket.h>.
  • Step 2 originally a second sendto β€” blocked: soisdisconnected() makes sosend fail EPIPE/SIGPIPE before ngc_send runs (observed EXIT=141). connect() is the route: soconnect has no disconnected-state gate.
  • Connect address must be node:hook (both parts) or ng_path_parse returns hook==NULL β†’ early EINVAL (:690) before the deref. shutme:link0 works.

Fix validation β€” FIXED (module-level)

fix.diff adds the missing NULL checks (mirroring ngd_send:357/ng_bind:714): - ngc_send: if (pcbp->sockdata == NULL) { error = ENOTCONN; goto release; } - ng_connect_cntl: if (sockdata == NULL) { TRAP_ERROR; return (ENOTCONN); }

Applied in guest /usr/src (repo sys/ untouched), module rebuilt (sys/netgraph/socket make), loaded over stock. Same PoC: connect returned -1 errno=57 (ENOTCONN), no panic, guest alive, and the NGM_SHUTDOWN good-path send still works (returns 52) β€” no regression.

Kernel refs

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
↓ fix.diffper-fix-DF-3516

Confirmed kernel references

Detail

Exploit chain

socket(AF_NETGRAPH,SOCK_DGRAM,NG_CONTROL) + bind name + sendto(NGM_SHUTDOWN to own node) [ngs_rmnode: sockdata=NULL, socket stays open] + connect(node:hook path) -> ng_connect_cntl -> NULL->node load at offset 0 -> page fault -> kernel panic

Evidence (decisive lines)

panic.txt + run.log (Fatal trap 12, vaddr 0x0, ngc_connect+0x52); run.fix.log (patched module: connect=-1 errno=57 ENOTCONN, guest alive); df3516.c (final PoC); fix.diff (validated)

PoC changes

Fixed NG_CONTROL (2, not 1) via system header; second sendto replaced with connect() after observing sosend's EPIPE/SIGPIPE gate (EXIT=141) blocks that route; connect address must be 'node:hook' (ng_path_parse rejects empty hook with EINVAL before the deref); added SIGPIPE ignore.

Verified recommended fix

Add pcbp->sockdata/sockdata NULL checks returning ENOTCONN in ngc_send and ng_connect_cntl (see fix.diff).

Verdict

Deterministic kernel NULL-deref panic reproduced on the stock guest: after NGM_SHUTDOWN shuts the socket node down (ngs_rmnode NULLs pcbp->sockdata and frees the ngsock while the control socket stays open), connect() on that socket passes soconnect (no SS_ISDISCONNECTED gate) into ng_connect_cntl which dereferences sockdata->node -> Fatal trap 12, fault vaddr 0x0, 'Stopped at ngc_connect+0x52: movq (%r13),%rdi'. Root-gated (control sockets require caps_priv_check SYSCAP_RESTRICTEDROOT), no write primitive, so impact is privileged local DoS, not privesc. Fix validated at module level: with the two NULL checks applied, the identical PoC returns ENOTCONN (errno 57) with no panic and the NGM_SHUTDOWN good path still works.