# DF-2573 — VERDICT

**status: reproduced — impact: panic (kernel NULL dereference / local DoS from
ng_device_newhook error paths). confidence: certain.**

**Crucial reachability fact discovered during verification:** the audit
target `sys/netgraph/ng_device.c` is **dead code in the stock build system**:

* absent from `sys/conf/files` (only `netgraph7/ng_device.c optional
  netgraph7_device` is registered — the netgraph7 rewrite is a different
  file and does not share these bugs),
* absent from the module `SUBDIR` list in `sys/netgraph/Makefile` (there is
  no ng_device.ko anywhere; guest `/boot/kernel` contains only `netgraph.ko`),
* and it no longer compiles against current headers at all: it programs the
  2003-era device API (`struct cdevsw`, `make_dev(cdevsw*)`,
  old-signature `d_*_t`, `cdevsw_add`, `ng_constructor_t(node_p)`) that has
  since been removed from the tree.

So on any stock DragonFly system this code cannot be reached — not by root,
not under memory pressure, not via kldload (the module does not exist).
Verification therefore compiled the file INTO a custom kernel
(`X86_64_NGDPOC`, stock `X86_64_GENERIC` INVARIANTS build plus
`options NETGRAPH`, `NETGRAPH_ECHO`, `NETGRAPH_SOCKET`, `device ngdpoc`)
using a shim that provides only the missing infrastructure (see below). The
bugs are real in the source text and mechanically deterministic once the
code is compiled; the finding stands as filed (Medium), with the dead-code
caveat front and center for triage (see Recommended disposition).

## The rig (verbatim interposition)

* `gen_ngdpoc.sh` builds `sys/netgraph/ng_device_poc.c` = shim prologue +
  `ng_device.c` + accessor epilogue. The verbatim region is verified
  byte-identical to `/usr/src/sys/netgraph/ng_device.c` by reconstruction
  and cmp; the pristine sha256 (7850d8ae…, identical to the audit tree) is
  baked into the binary and printed at every boot
  (`NGDPOC: verbatim ng_device.c sha256=… conn_size=40`).
* EXACTLY ONE line differs from pristine, asserted by the generator
  (`diff | grep -c '^< '` == 1): `ng_device_cons(node_p node)` →
  `ng_device_cons(node_p *node)` — aligning the (never-invoked,
  always-EINVAL) constructor with the current `ng_constructor_t` ABI that
  the tree changed under the file. The functions under test —
  `ng_device_newhook` (ng_device.c:263-312), `ng_device_disconnect`
  (385-414), `get_free_unit` (224-258) — are byte-identical.
* The shim `make_dev()` forwards to the REAL `make_dev()`/devfs through a
  trampoline `dev_ops`, so a REAL `/dev/ngd0` appears (verified: crw-------,
  data written to /dev/ngd0 traversed the echo netgraph node and was read
  back). Fault injection is scoped to this file only:
  * make_dev failure: armed via `kern.ngdpoc.fail_makedev` (shim returns
    NULL on the Nth call from ng_device.c);
  * readq kmalloc failure: one-shot `kmalloc(10240, M_DEVBUF)` failure,
    patched at the entry of `_kmalloc`/`_kmalloc_debug`
    (sys/kern/kern_slaballoc.c, guest-only interposer `ngdpoc_slab.diff`),
    armed and consumed inside the same sysctl call.
* The driver is driven through the REAL netgraph machinery — the harness
  calls the exported `ng_mkpeer()` / `ng_destroy_hook()` /
  `ng_findname()` (exactly what ngctl mkpeer/rmhook drive at
  ng_base.c:1270/1314), and the pure-userland path was also exercised with
  /usr/sbin/ngctl over the compiled-in ng_socket.

## Baseline results (vulnerable kernel #0, built 23:52 UTC)

**Variant B — non-empty list, injected readq kmalloc failure
(ng_device.c:300).** One live connection first (`conn=1 … unit=0`), then
`sysctl -n "kern.ngdpoc.trigger=mkpeer_failreadq tB"`:

```
NGDPOC: armed one-shot kmalloc(10240, M_DEVBUF) failure
ng_device_newhook()
get_free_unit()
NGDPOC: injected kmalloc(10240, M_DEVBUF) failure
ng_device_newhook(): readq malloc failed
Fatal user address access from kernel mode from sysctl at ffffffff80a87223
Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0x0
Stopped at      ng_device_newhook+0x1f3:  movq    (%rdx),%rax
db>
```
(`panic.B.console.txt`; guest wedged in DDB, ssh dead, vm status down.)

**Variant A — empty list, injected make_dev failure (ng_device.c:290).**
Fresh boot (list empty, M_DEVBUF baseline), `kern.ngdpoc.fail_makedev=1`,
then trigger:

```
ng_device_newhook()
get_free_unit()
NGDPOC: injected make_dev() failure
ng_device_newhook(): make_dev failed
Fatal user address access from kernel mode from sysctl at ffffffff80a871d3
Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0x0
Stopped at      ng_device_newhook+0x1a3:  movq    (%rdx),%rax
db>
```
(`panic.A.console.txt`.)

**Line-accurate confirmation:** with `-g` DWARF from the built object
(`trapsite.disasm.txt`), both trap instructions resolve to the two
SLIST_REMOVE lines:
* `ng_device_newhook+0x1a3` → ng_device_poc.c:480 → **ng_device.c:290**
  (make_dev error path),
* `ng_device_newhook+0x1f3` → ng_device_poc.c:490 → **ng_device.c:300**
  (readq error path).

## Mechanics (why it always panics)

`SLIST_REMOVE(head, elm, …)` (sys/sys/queue.h:208-220) on an element that
was never `SLIST_INSERT`ed (insert happens only at ng_device.c:309, after
every failure point):

* **empty list**: `SLIST_FIRST(head)==NULL != elm` → else-branch sets
  `curelm = NULL` → `SLIST_NEXT(curelm, links)` reads address 0x0 → fatal
  trap 12 (fault virtual address = 0x0 — exactly what both panics show).
* **non-empty list**: `curelm` walks the real connections; `elm` is not in
  the list, so `curelm` runs past the last element to NULL and the next
  `SLIST_NEXT(NULL)` faults identically.

No KASSERT is involved; this is a plain NULL dereference on INVARIANTS and
production kernels alike. Preconditions in a hypothetical live deployment:
root (netgraph configuration) + a make_dev or 10 KB M_DEVBUF allocation
failure under memory pressure — M_NOWAIT failures are routine under load on
DragonFly (see malloc.h M_NOWAIT commentary).

## Fix validation (fix.diff applied to guest /usr/src, incremental rebuild)

`fix.diff` deletes both SLIST_REMOVE calls (the connection is only linked
at :309, after all failure points; the error paths already kfree the
connection). The POC TU was regenerated from the FIXED source (boot attest
sha256 249587a7…) and the exact PoCs re-run (`run.fix-validation.log`):

* **A**: injected make_dev failure → `ng_device_newhook(): make_dev failed`,
  `ng_mkpeer(fixA, type echo) = -1`, conn=0, **guest alive, no panic**.
* **B**: injected readq failure (with a live connection on the list) →
  `readq malloc failed`, destroy_dev cleanup, `ng_mkpeer(fixB) = -1`,
  list intact (conn=1), **guest alive, no panic**.
* Normal operation unchanged: hook create, `FIXOK` round-trip through
  /dev/ngd0 via the echo node, clean disconnect; M_DEVBUF returns to
  baseline (inuse 211).

## Impact ceiling and disposition

* Primitive: attacker-timed NULL-page READ from kernel mode → guaranteed
  panic / local DoS. No write primitive, no info leak; not an escalation.
* Privilege needed to reach it on a system where the file were built: root
  (netgraph node configuration via ngctl; nodes cannot be created by
  unprivileged users, and stock kernels ship no netgraph at all).
* On stock systems: unreachable (dead code — see top).
* Per the team directive (panic + leak, root-gated netgraph — close or
  flag): **close as panic**, with the dead-code status recorded; the
  one-line-each fix (or outright removal of the bit-rotted file) is
  validated above.

## Environment

* Guest: DragonFly 6.5-DEVELOPMENT x86_64, custom `X86_64_NGDPOC`
  INVARIANTS kernel (gcc 8.3 / cc80), 6 vCPU, 4 GB RAM, QEMU/KVM
  (`env.txt`).
* `make -j6 nativekernel` (buildkernel is gated by bk_tools on this
  snapshot) + `make installkernel DESTKERNNAME=kernel` (`build.sh`;
  `build.log` is the full untrimmed log of the final rebuild — the two
  earlier baseline build logs were overwritten in /root/build_ngdpoc.log
  by subsequent rebuilds; the procedure is byte-identical, and the
  baseline kernel is attested in-run by the boot line
  `sha256=7850d8ae…` plus `#0: … 23:52` in run.leak.log).
* Harness behavior notes (documented for reproducers): DF sysctl(8)
  string-writes retry the handler while it returns an error — the harness
  trigger therefore always returns 0 and reports results via kprintf
  (df_ngdpoc.c header comment).

## Reproduce

```
./build.sh BASELINE      # ~12 min full build, installs + reboots
vm.sh run_root 'sysctl -n kern.ngdpoc.stats'        # list empty, baseline
vm.sh run_root 'sysctl kern.ngdpoc.fail_makedev=1; sysctl -n "kern.ngdpoc.trigger=mkpeer tA"'
# -> panic "Fatal trap 12 ... Stopped at ng_device_newhook+0x1a3: movq (%rdx),%rax"
# variant B: mkpeer keepB first, then trigger=mkpeer_failreadq tB -> panic at +0x1f3
./build.sh FIX           # applies fix.d/*.diff, rebuilds from fixed source
# re-run: clean -1 returns, no panic (run_fix_validation.sh)
```

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