# DF-3511 — VERDICT

**Status: reproduced** (leak/DoS, root-gated) · **fix validated in-guest**

## What was claimed

`ngc_send()` intercepts `NGM_MKPEER` whose type is not loaded
(ng_socket.c:276-307). If `linker_api_available()` returns 0 it takes
`error = ENXIO; goto done;` (ng_socket.c:285-288). `done:` (ng_socket.c:351)
is after the `release:` block (ng_socket.c:344-350) that frees `path`,
`control` and `m`, and `msg` (kmalloc'd at ng_socket.c:261) has no free on
this path at all. Every sendto(2) that hits the branch leaks three
allocations: the ng_mesg buffer (user-chosen `len+1` bytes), the path buffer,
and the datagram mbuf.

## How it was proven

Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC), netgraph7 built from
/usr/src as modules (stock /boot modules are the old sys/netgraph stack).
Trigger: root opens an AF_NETGRAPH NG_CONTROL socket and sends a correctly
laid-out (netgraph7 header) NGM_MKPEER addressed to its own node ("."),
50k iterations per phase:

* **Phase A (control)** — type "socket" (loaded): the loader block is
  skipped, generic MKPEER fails in `ngs_constructor`. Result: 50,000x EINVAL,
  free pages 839636 -> 839220 (background noise), mbufs in use 14 -> 12.
  **No leak.**
* **Phase B** — type "df3511x" (never loaded): `ng_findtype()` returns NULL,
  `linker_api_available()` returns 0 (see below) -> ENXIO + `goto done`.
  Result: 50,000x **ENXIO**, free pages 839220 -> **830848 (-8,372 pages ≈
  33.5 MB, ~683 bytes/send)**, mbufs in use 12 -> **50,012 — exactly one
  leaked mbuf per sendto**. run.log, run.2.log-equivalent numbers inline.

The 1:1 mbuf fingerprint (50000 sends -> +50000 mbufs, current/max counter)
removes any doubt that the growth is coincidental.

## Why `linker_api_available()` is always 0 here

`linker_api_available()` (ng_socket.c:1175-1188) returns 0 when
`curproc == NULL`. On DragonFly, `pru_send` handlers run on the socket's
protocol (lwkt) thread, which has no process context — so *every* MKPEER
naming an unloaded type takes the leak path. This also means module autoload
through AF_NETGRAPH control sockets is functionally broken (always ENXIO) on
this kernel — corroborating that this branch had effectively never been
exercised with a correct netgraph7 message before.

## PoC iteration notes (what had to be fixed)

1. v1 used the stock userland `<netgraph/ng_message.h>` (old sys/netgraph
   layout, NG_VERSION 2→overridden 8). The old header layout does NOT match
   netgraph7 (ng7: arglen@+4, cmd@+8, flags@+12, token@+16, typecookie@+20,
   cmdstr[32] -> 56-byte header): the kernel silently mis-parsed v1 messages
   (they fell through to `ngs_rcvmsg` and were echoed back to the socket as
   datagrams — sendto appeared to "succeed"). Diagnosed with `diag.c`
   (response dump: foreign cookie field, echoed payload).
2. v2 hand-rolls the ng7 kernel layout — immediately produced the expected
   EINVAL (control) / ENXIO (leak) split.
3. Memory accounting: `vmstat -m` is unreliable for kld-loaded malloc types
   on this guest (same observation as DF-3474) — used
   `vm.stats.vm.v_free_count` + `netstat -m` mbuf counters instead.

## Fix validation

`fix.diff` (authored against the repo's read-only sys/ tree, applied to the
guest's /usr/src copy only):

* hunk 1: `kfree(msg, M_NETGRAPH_MSG); error = ENXIO; goto release;`
  (mirrors the sibling exits at ng_socket.c:294-297);
* hunk 2: remove the now-unused `done:` label (-Werror=unused-label).

Rebuilt ng_socket.ko in-guest, rebooted (ng_socket.ko cannot be kldunloaded:
`ngs_mod_event` MOD_UNLOAD returns EBUSY), kldloaded the patched module and
re-ran the identical workload (fix_run.log):

* phase A: 50,000x EINVAL (unchanged), free 870303 -> 869306;
* phase B: 50,000x **ENXIO (behavior preserved)**, free 869306 -> **869202**
  (flat vs -8,372 baseline), **mbufs in use 7 -> 7** (baseline: +50,000).

Bad behavior gone; no functional regression visible from the socket API.

## Impact ceiling

Root-gated (control socket requires `caps_priv_check(SYSCAP_RESTRICTEDROOT)`,
ng_socket.c:182-184). Unbounded kernel heap + mbuf leak per sendto →
memory-exhaustion DoS by a privileged user (no unprivileged trigger, no
memory corruption, no info disclosure). The identical ENXIO side-effect shows
module autoload via ng sockets is broken, which is a functional bug in its
own right. Severity: Low.

Guest was reset to the `with-src` snapshot after validation; `/usr/src` in
the repo tree was never modified.

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