# DF-2574 — VERDICT

**status: reproduced — impact: dos (unbounded kernel heap leak, 40 bytes of
M_DEVBUF per connect/disconnect cycle, never reclaimed; root-gated,
dead-code reachability — see below). confidence: certain.**

## What was verified and how

Same rig as DF-2573 (see that pack; shared sources copied here): the
byte-verbatim `sys/netgraph/ng_device.c` compiled into the
`X86_64_NGDPOC` INVARIANTS kernel (pristine sha256 7850d8ae… attested at
boot, `sizeof(struct ngd_connection) == 40` attested at boot), driven
through the REAL netgraph paths:

* in-kernel: exported `ng_mkpeer()` (what ngctl mkpeer drives at
  ng_base.c:1270) + `ng_destroy_hook()` (ng_base.c:1314) on the real
  boot-created `device` node;
* pure userland: `/usr/sbin/ngctl mkpeer device: echo aN bN` +
  `ngctl rmhook` over the compiled-in ng_socket.

`ng_device_disconnect` (ng_device.c:385-414) frees `readq`
(kfree at :407), destroys the cdev (:409) and unlinks the connection
(SLIST_REMOVE at :411) — but never kfrees the `struct ngd_connection`
itself, allocated in `ng_device_newhook` at :273 with
`kmalloc(sizeof(struct ngd_connection), M_DEVBUF, M_NOWAIT)`.
get_free_unit() recycles unit numbers of removed connections, so the
cycle can repeat without bound.

## Measurements (exact in-kernel M_DEVBUF malloc-type accounting:
`ks_use[cpu].{memuse,inuse,calls}` summed over all CPUs; noise floor
verified flat — three idle samples, zero drift)

| stage | cycles | memuse delta | inuse delta | calls delta |
|---|---|---|---|---|
| control (idle, 18 s) | 0 | 0 | 0 | 0 |
| warmup | +100 | +4000 | **+100** | +200 |
| leak run 1 | +20000 | +800000 | **+20000** | +40000 |
| leak run 2 | +20000 | +800000 | **+20000** | +40000 |
| ngctl (userland path) | +500 | +20000 | **+500** | +1000 |

Exactly **40 bytes and exactly 1 never-freed in-use allocation per cycle**,
perfectly linear, after which the connection list is empty (conn=0) and
/dev/ngd0 is gone — the memory is leaked kernel heap that nothing reclaims
(final state after 40500 cycles: inuse 212 → 40812, memuse +1.62 MB;
`run.leak.log`). The readq (10240 B) IS freed each cycle; only the 40-byte
connection leaks (calls delta = 2 per cycle, inuse delta = 1). The leak is
unbounded over time; ~26 M cycles would exhaust 1 GB of kernel heap with
nothing but root netgraph syscalls.

## Fix validation (fix.diff = add `kfree(connection, M_DEVBUF);` after the
SLIST_REMOVE in ng_device_disconnect; applied to guest /usr/src, POC TU
regenerated from the fixed source, attest 249587a7…)

* `cycle 20000` on the fixed kernel: **inuse delta 0, memuse delta 0**
  (calls +40000 — the allocations still happen and are now all freed;
  `run.fix-validation.log`).
* Normal operation unchanged: hook create, `FIXOK` round-trip through
  /dev/ngd0 via the echo node, disconnect returns M_DEVBUF to baseline
  (inuse 211).

## Impact ceiling and disposition

* Resource-exhaustion DoS class (not an info leak; nothing is copied out —
  the leaked bytes never leave the kernel). Requires root to drive
  netgraph, and root can exhaust memory through many normal interfaces
  anyway, so the practical severity delta is low.
* Material reachability caveat: `sys/netgraph/ng_device.c` is dead code in
  the stock build system (absent from `sys/conf/files` and the module
  SUBDIR list; it does not even compile against current headers — see the
  DF-2573 VERDICT). Stock systems cannot reach this code.
* Per the team directive (panic + leak, root-gated netgraph — close or
  flag): **close as a leak/doS with the dead-code status recorded**; the
  one-line fix is validated above.

## Environment

Same as DF-2573 (`env.txt`): DragonFly 6.5-DEVELOPMENT x86_64,
X86_64_NGDPOC INVARIANTS kernel, gcc 8.3, 6 vCPU, QEMU/KVM; build via
`make -j6 nativekernel` (`build.sh`, full log in `build.log`).

## Reproduce

```
./build.sh BASELINE
./run.sh                  # run_df2574_leak.sh: control + 100 + 2x20000
                          # in-kernel cycles + 500 ngctl cycles
                          # expect: +40 bytes and +1 inuse per cycle
./build.sh FIX            # push fix.diff to /tmp/fix.d first
./run_fix_validation.sh   # expect: cycle 20000 -> inuse delta 0
```

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