⬢ DragonFlyBSD Kernel Audit
DF-3511 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-3511",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "dos",
  "confidence": "certain",
  "verdict": "ngc_send()s NGM_MKPEER autoload interception (ng_socket.c:276-307) leaks three allocations whenever linker_api_available() returns 0: it jumps to done: (ng_socket.c:287->351), which is past the release: block that frees path/control/m (ng_socket.c:344-350), and msg (kmalloc(len+1, M_NETGRAPH_MSG), ng_socket.c:261) is never freed on this path. On DragonFly pru_send handlers run on a protocol thread with curproc==NULL, so linker_api_available() (ng_socket.c:1175-1188) returns 0 on EVERY send - the branch is the sole outcome of MKPEER-with-unloaded-type, not a boot-time corner case. Reproduced in-guest with a netgraph7-layout trigger: 50k bogus-type sendto -> 50000x ENXIO, 33.5MB (8372 pages) of kernel memory permanently leaked (~683 B/send), and netstat -m mbufs-in-use grew by EXACTLY one per send (12 -> 50012) - a 1:1 fingerprint; control phase (loaded type, EINVAL) was flat. Corollary functional bug: module autoload through AF_NETGRAPH sockets never works (always ENXIO). Root-gated (control socket: caps_priv_check(SYSCAP_RESTRICTEDROOT), ng_socket.c:182-184), so ceiling is resource-exhaustion DoS by a privileged user - no unprivileged route, no memory corruption, no disclosure. fix.diff (kfree(msg)+goto release, mirroring the sibling exits at :294-297, plus removal of the now-unused done: label) validated in-guest: identical EINVAL/ENXIO behavior with flat memory and mbufs 7->7 across the same 100k-send workload.",
  "exploit_chain": "root -> socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL) -> sendto(NGM_MKPEER, type=<any not-yet-loaded type>) -> ngc_send: ng_findtype()==NULL, linker_api_available()==0 -> ENXIO + goto done -> msg + path + mbuf leaked per send -> repeat: unbounded kernel heap + mbuf-pool exhaustion (mbuf max 146632 on this guest -> ~100k sends caps all further mbuf allocation system-wide; kmem likewise drains). Stop at exhaustion = permanent local DoS; requires root throughout.",
  "evidence": "run.log - baseline A/B: A 50000x EINVAL free 839636->839220 mbufs 14->12; B 50000x ENXIO free 839220->830848 (-8372 pages), mbufs 12->50012 (exactly +1/send); fix_run.log - patched module, same workload: B still 50000x ENXIO but free 869306->869202 (flat) and mbufs 7->7; trigger.c - ng7-layout MKPEER trigger; diag.c - header-layout mis-parse discovery tool; VERDICT.md - full narrative incl. why curproc==NULL makes the branch always-taken; fix.diff - 2-hunk validated fix",
  "kernel_refs": [
    "sys/netgraph7/socket/ng_socket.c:285",
    "sys/netgraph7/socket/ng_socket.c:287",
    "sys/netgraph7/socket/ng_socket.c:261",
    "sys/netgraph7/socket/ng_socket.c:246",
    "sys/netgraph7/socket/ng_socket.c:344",
    "sys/netgraph7/socket/ng_socket.c:351",
    "sys/netgraph7/socket/ng_socket.c:294",
    "sys/netgraph7/socket/ng_socket.c:182",
    "sys/netgraph7/socket/ng_socket.c:1175"
  ],
  "poc_changes": "v1 used the stock userland <netgraph/ng_message.h> (old sys/netgraph layout); netgraph7 uses a different 56-byte header (arglen@+4, cmd@+8, flags@+12, token@+16, typecookie@+20, cmdstr[32]@+24), so v1 messages were silently mis-parsed (echoed back by ngs_rcvmsg, sendto appeared to succeed). v2 hand-rolls the ng7 kernel layout; the ENXIO/EINVAL split appeared immediately. Memory measurement switched from vmstat -m (unreliable for kld-loaded malloc types on this guest, per DF-3474) to vm.stats.vm.v_free_count + netstat -m mbuf counters. Fix needed a second hunk (drop the done: label) because -Werror=unused-label breaks the module build otherwise.",
  "attempts": 4,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64",
  "runtime_sec": 1500,
  "guest_dirty": 0,
  "build_cmd": "sh /root/df3511/build.sh  (netgraph7 netgraph.ko+ng_socket.ko from /usr/src/sys/netgraph7; cc -O2 -Wall -o /root/df3511/trigger /root/df3511/trigger.c)",
  "run_cmd": "kldload /root/ng7/netgraph.ko && kldload /root/ng7/ng_socket.ko; /root/df3511/trigger loaded 50000; /root/df3511/trigger bogus 50000; sysctl -n vm.stats.vm.v_free_count; netstat -m",
  "code_hash": "f6634db2fe8d395846cf3aa93a7a83b9e60576453f5fbfe1243da604cce23e76",
  "notes": "Guest reset to with-src snapshot after validation; repo sys/ tree untouched. ng_socket.ko cannot be kldunloaded (ngs_mod_event MOD_UNLOAD -> EBUSY), so fix validation required a reboot with the rebuilt module. Side facts: (a) module autoload via AF_NETGRAPH is functionally always-ENXIO on DFly (curproc==NULL on the protocol thread) - separate functional bug, same root; (b) mbuf ceiling 146632 on this guest -> ~100k more bogus sends after the demonstrated 50k would cap system-wide mbuf allocation.",
  "recommended_fix": "In ngc_send()s MKPEER loader block, free msg and take the release path when the linker API is unavailable (kfree(msg, M_NETGRAPH_MSG); error = ENXIO; goto release;) and delete the then-unused done: label - see fix.diff.",
  "fix_status": "fixed",
  "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0 (netgraph7 ng_socket.ko rebuilt in-guest with fix.diff, kldloaded after reboot)",
  "fix_baseline_reproduced": 1,
  "fix_notes": "Patched module: same 100k-send workload -> EINVAL/ENXIO behavior identical, free-page delta in leak phase collapsed from -8372 pages to -104 (noise), mbuf delta from +50000 to 0. fix_run.log in pack. /usr/src in-guest patch reverted by final vm.sh reset with-src."
}