# DF-0696 — Missing return after NG_FREE_DATA in ng_etf_rcvdata

## Verdict: NOT REPRODUCED (bug is structurally real; trigger requires root + a tight race)

## Mechanism (sys/netgraph/etf/ng_etf.c:381-392)

```c
if (NG_HOOK_PRIVATE(hook) == NULL) { /* Shouldn't happen but.. */    /* line 381 */
    NG_FREE_DATA(m, meta);           /* sets m = NULL, meta = NULL */ /* line 382 */
}                                                                    /* line 383: NO return! */

...
if (m->m_len < sizeof(*eh)) {          /* line 392: m == NULL -> page fault */
```

`NG_FREE_DATA` (netgraph.h:283-287) is a `do{ NG_FREE_M(m); NG_FREE_META(a); }while(0)`
where both inner macros NULL their argument. The "Shouldn't happen but.."
comment shows the original intent was clearly free-and-bail; the bail was
forgotten. Falling through, `m->m_len` reads from NULL → page fault.

## Trigger

The condition `NG_HOOK_PRIVATE(hook) == NULL` becomes true during
`ng_etf_disconnect` (line 491) — which sets HK_INVALID **and** sets
private to NULL — racing with `ng_send_data` (ng_base.c), which checks
HK_INVALID without a lock and then synchronously invokes `rcvdata`. If
the sender's check passes just before disconnect runs, `rcvdata` observes
`private == NULL` and hits the bug.

## Reachability

- ng_socket creation is **root-only** (CAP_NETGRAPH + the standard
  netgraph privilege check). An unprivileged user cannot create the etf
  node or drive data through it.
- The race window is narrow: the sender must read `HK_INVALID==0`, then
  `ng_etf_disconnect` must set HK_INVALID and private=NULL, then `rcvdata`
  must observe private==NULL — all between two specific instructions.

A simple ngctl-based loop (`race.sh`) did not fire in 200 iterations.
Driving raw mbuf data into an etf match hook from userspace would
require either an ng_socket data socket or a real `ng_ether` push path,
which `ngctl` doesn't expose directly; a focused C PoC using
`socket(AF_NETGRAPH, SOCK_DGRAM, ...)` with two parallel threads
(producer + teardown) is the natural next iteration.

## Why I stopped here

Even if the race fired:
- It is a **DoS (panic)** only — `NG_FREE_DATA` NULLs both pointers, so
  there is no UAF or arbitrary-write primitive. The freed mbuf is gone,
  not dangling.
- The path is **root-only** (netgraph setup). Root→kernel is game-over.
- An unprivileged user cannot reach this code at all.

Per the realistic-threat test, this is a root→kernel DoS hardening gap,
not an unprivileged escalation. The bug is unambiguous from the source
(missing `return` after `NG_FREE_DATA`) and is fixed trivially.

## Fix

`fix.diff` adds `return (EINVAL);` immediately after `NG_FREE_DATA`:

```diff
@@ -380,6 +380,7 @@
 
 	if (NG_HOOK_PRIVATE(hook) == NULL) { /* Shouldn't happen but.. */
 		NG_FREE_DATA(m, meta);
+		return (EINVAL);			/* DF-0696: missing bail-out */
 	}
```

Matches the apparent original intent (free + bail with EINVAL, same as
the short-packet branch at line 396).

## Files

- `race.sh` — ngctl-based tight connect/disconnect loop (control-msg only;
  did not fire in 200 iterations on the default kernel)
- `build.sh` / `run.sh` — shell-only PoC wrappers
- `run.log` — sample race attempt output (no panic)
- `VERDICT.md` — this analysis
- `fix.diff` — return(EINVAL) after NG_FREE_DATA
