# DF-0759 — PoC & reproduction

**Double `NG_FREE_ITEM` on the ng_split out-hook data path** (Low severity,
CWE-672). A redundant `NG_FREE_ITEM(item)` in the out-hook branch of
`ng_split_rcvdata()` is followed unconditionally by a catch-all
`if (item) NG_FREE_ITEM(item)`. The non-debug `NG_FREE_ITEM` does not null
`item`, so the catch-all always runs and trips `KKASSERT(!(NGQF_FREE))` →
panic on INVARIANTS kernels. On production (non-INVARIANTS) kernels the
second call is a harmless idempotent flag-set.

## Reachability & threat model

- **Root-only.** Delivering data to a split node's `out` hook requires a
  netgraph topology, which requires the privileged netgraph control socket.
- **Non-default module.** `ng_split` lives only in `sys/netgraph7/` (the opt-in
  parallel netgraph stack). It is NOT in `X86_64_GENERIC` and NOT in the default
  module build. The guest ships the old `sys/netgraph/` stack, which has no
  `split` node at all.
- **Not memory corruption.** `NG_FREE_ITEM` only sets a mark-for-later-free
  flag; the actual `ng_free_item()` runs once. No slab double-free, no UAF, no
  write primitive — hence no escalation path.

## Files

| file         | purpose                                                       |
|--------------|---------------------------------------------------------------|
| `inject.c`   | userland trigger: builds socket→split:out topology, sends 1 data byte |
| `build.sh`   | compiles `inject` against `libnetgraph`                       |
| `run.sh`     | builds netgraph7 core + ng_socket7 + ng_split (`KCFLAGS=-DINVARIANTS`), loads them, fires `inject` |
| `fix.diff`   | git-apply-able fix: drop the redundant `NG_FREE_ITEM`         |
| `panic.txt`  | baseline panic signature from `boot.log` (unpatched)          |
| `VERDICT.md` | full narrative + before/after                                 |
| `manifest.json` | machine-readable catalog                                   |

## How to reproduce (as root, on the DragonFly audit guest)

The trigger must run as **root** (netgraph control socket). `run.sh` does
everything from a clean boot:

```
ssh -F dfbsd-qemu/config dfbsd    # root
cd /home/maxx/poc/DF-0759 && sh run.sh
```

What `run.sh` does:
1. builds `netgraph7` core + `ng_socket7` from `/usr/src/sys/netgraph7/`;
2. builds `ng_split` from `/usr/src/sys/netgraph7/ng_split.c` with
   `KCFLAGS=-DINVARIANTS` so the `KKASSERT` inside `NG_FREE_ITEM` is live
   (`bsd.kmod.mk` does not inherit the kernel's INVARIANTS, so a stock kld
   build would have the assertion compiled out — this reproduces the
   `options NETGRAPH7_SPLIT` + INVARIANTS kernel the finding describes);
3. builds/installs `libnetgraph7` (NG_VERSION=8 — the stock `ngctl`/libnetgraph
   is NG_VERSION=2 and is rejected by netgraph7's `ng_socket`);
4. loads the netgraph7 stack + `ng_split`;
5. compiles `inject` and fires one data byte at `split:out`.

### Expected output

**Bug present (unpatched, INVARIANTS active):** the guest panics — ssh dies and
the serial log (`dfbsd-qemu/boot.log`) shows:

```
ng_split: got packet from out hook!
panic: assertion "!(item->el_flags & NGQF_FREE)" failed in ng_split_rcvdata at .../ng_split.c:145
ng_split_rcvdata() at ng_split_rcvdata+0xfd
ng_apply_item() at ng_apply_item+0x105
ngthread() at ngthread+0x18
```

**After `fix.diff` (same trigger):** `inject` prints `NO_PANIC`, the guest stays
up, `dmesg` shows `ng_split: got packet from out hook!` (the path still runs) but
no panic.

## Validating the fix

```
scp fix.diff dfbsd:/root/fix.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 < /root/fix.diff'   # apply
ssh -F dfbsd-qemu/config dfbsd 'cd /home/maxx/poc/DF-0759 && sh run.sh'      # rebuild + fire
# -> NO_PANIC, guest up
```
