# DF-3547 VERDICT — UAF of `struct ngd_private` (ng7 ng_device) vs async `destroy_dev()`

**Status: build_failed / not runtime-testable** (the containing file cannot be compiled
into any DragonFly kernel — proven on the guest, see build.log). The defect itself is
**certain by source-level analysis**; every link in the chain is cited below.

## The bug

`ng_device_disconnect()` (sys/netgraph7/ng_device.c:292-312) tears the node down when
its hook is removed:

```c
299:  destroy_dev(priv->ngddev);          /* (1) ASYNCHRONOUS in DragonFly */
300:  mtx_destroy(&priv->ngd_mtx);
302:  IF_DRAIN(&priv->readq);             /* frees every queued mbuf */
303:  mtx_destroy(&(priv)->readq.ifq_mtx);
305:  free_unr(ngd_unit, priv->unit);
307:  kfree(priv, M_NETGRAPH);            /* (2) priv freed immediately */
```

(1) In DragonFly, `destroy_dev()` (sys/kern/kern_conf.c:345-354) only calls
`devfs_destroy_dev()` (sys/vfs/devfs/devfs_core.c:759-763), which calls
`devfs_msg_send()` (devfs_core.c:1113-1127) — a **fire-and-forget `lwkt_sendmsg`**
to the devfs core thread. It returns *immediately*: it does not wait for in-flight
cdevsw method invocations to drain, does not wait for blocked readers, and does not
revector the device's ops. `devfs_destroy_dev_worker()` (devfs_core.c:1440-1459) merely
unlinks the cdev and drops sysrefs. This is unlike FreeBSD, whose `destroy_dev()`
drains cdevsw users — the ng7 file was imported from FreeBSD assuming those semantics.

(2) Meanwhile every device method fetches the private structure straight from the cdev:

* `ngdopen`   ng_device.c:334  `priv = dev->si_drv1;` → :338-340 `mtx_lock(&priv->ngd_mtx); priv->flags |= NGDF_OPEN;`  — **UAF write** into the freed chunk
* `ngdclose`  ng_device.c:351  `priv = dev->si_drv1;` → :354-356 same write
* `ngdread`   ng_device.c:411  `priv = dev->si_drv1;` → :419 `IF_DEQUEUE(&priv->readq, m)` (queue drained+freed at :302 → dangling mbufs → `m_free()` of freed mbufs = **double free**), :423-427 `msleep(priv, &priv->ngd_mtx, ...)` on a destroyed, freed mutex
* `ngdwrite`  ng_device.c:454  `priv = dev->si_drv1;` → :469 `NG_SEND_DATA_ONLY(error, priv->hook, m)` — dereferences `priv->hook` (freed memory read) and, if the stale value survives, hands the mbuf to a **freed hook pointer**, walking `NG_HOOK_PEER`/`NG_PEER_NODE` (ng_address_hook, ng_base.c:3062-3065)
* `ngdpoll`   ng_device.c:481  `priv = dev->si_drv1;` → :485 `IFQ_IS_EMPTY(&priv->readq)` — UAF read

Why a stale path exists at all: an *open* file descriptor keeps the vnode → `v_rdev`
alive (sys/vfs/devfs/devfs_vnops.c:1272-1295 `devfs_spec_read` → `reference_dev(dev)`
→ `dev_dread(dev, ...)` → `release_dev(dev)`), and the destroy path never clears
`si_drv1` nor revokes ops — so every later `read(2)/write(2)/poll(2)` on the
still-open fd dispatches `ngdread/ngdwrite/ngdpoll` with `priv` already freed.
Additionally, because `destroy_dev()` is async, even a fresh `open("/dev/ngd0")` that
wins the race against the devfs core thread's unlink reaches `ngdopen` with dangling
`si_drv1`.

## Trigger (if the node type were buildable)

```
ngctl mkpeer . device dl dl      # root: creates ng_device node + hook, /dev/ngd0 appears
./stalefd &                      # opens /dev/ngd0, parks on the fd
ngctl rmhook <device-node> dl    # -> ng_device_disconnect -> destroy_dev(async) + kfree(priv)
# resume stalefd: read(fd)/write(fd) on the freed priv
```
`read()` first dequeues from the drained-then-freed queue (double free of the mbufs that
`IF_DRAIN` already freed) — an INVARIANTS/objcache panic is the expected signature;
`write()` drives the freed `priv->hook` into netgraph dispatch. With heap grooming
(spraying `M_NETGRAPH`-sized chunks — e.g. more ng node privates) this is a
controlled-ish UAF-write/arb-walk primitive. Both sides require root (device is
`UID_ROOT/GID_WHEEL 0600`, ng control sockets require `caps_priv_check(SYSCAP_RESTRICTEDROOT)`
— sys/netgraph7/socket/ng_socket.c:182-184), so the realistic ceiling is
root→kernel corruption, not unpriv→root.

## Why it could NOT be verified at runtime (hard blocker, demonstrated)

1. `sys/netgraph7/Makefile` SUBDIR has no `device` entry → no module is ever built
   (`/boot/kernel/ng_device.ko` absent on the guest, `ls` in env.txt).
2. `sys/conf/files:1699` gates it behind `optional netgraph7_device`; no kernel config
   in the tree defines it (`grep -rn NETGRAPH7 /usr/src/sys/config/*` → 0 hits).
3. The file does not compile: out-of-tree kmod build on the guest produces 64 errors —
   `'struct cdevsw' has no member named 'd_open'`, `unknown type name 'd_poll_t'`,
   `'D_VERSION' undeclared`, `'MTX_DEF' undeclared`, `'struct ifqueue' has no member
   named 'ifq_mtx'`, `implicit declaration of 'IF_LOCK'`, `implicit declaration of
   'IFQ_IS_EMPTY'`, `implicit declaration of 'msleep'`… (full log: build.log).
   These are FreeBSD-only primitives that never existed in DragonFly.

A faithful runtime demonstration would require first completing the FreeBSD→DF port
(rewriting the cdevsw as `struct dev_ops`, `msleep`→`ssleep`/`tsleep` with the
mutex-releasing semantics, the ifq shim…), i.e. executing the maintainer's porting work
and then testing code that is no longer byte-identical to the tree. That was judged to
add no evidentiary value beyond the cited source chain above, so the honest
classification is **build_failed / not runtime-testable**, defect **certain** by
code reading.

## Kernel references
- sys/netgraph7/ng_device.c:292-312 (disconnect free path), :332-343 (ngdopen),
  :348-359 (ngdclose), :408-443 (ngdread), :451-472 (ngdwrite), :478-489 (ngdpoll)
- sys/kern/kern_conf.c:345-354 (destroy_dev → devfs_destroy_dev, async)
- sys/vfs/devfs/devfs_core.c:759-763, :1113-1127 (async lwkt_sendmsg), :1440-1459 (worker)
- sys/vfs/devfs/devfs_vnops.c:1272-1295 (open fd keeps dispatching via si_drv1)
- sys/netgraph7/socket/ng_socket.c:182-184 (root-only control plane — bounds impact)

## Recommended fix
Reference-count `priv`: acquire under `ngd_mtx` in every cdevsw method (check a
`dying` flag), drop the node's base reference in `disconnect` *after* clearing
`si_drv1` under the same lock, and free `priv` only when the last method reference
returns — see fix.diff. (The file also needs its port finished or removed before any
of this is reachable; tracked as DF-3549.)

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