# DF-3530 — VERDICT

**Status: reproduced (panic, via deterministic post-teardown state harness).
Natural race attempted honestly: no hit in 20,339 cycles / ~2.6M frames.
Fix validated: patched module passes the identical harness test.**

## The bug

`ng_ether_detach()` (`sys/netgraph/ether/ng_ether.c:321-337`) tears the ether
node down and frees its private data with **no synchronization at all** against
the data-plane readers:

```c
ng_rmnode(node);                 /* :328 destroy hooks (HK_INVALID set)   */
node->flags |= NG_INVALID;       /* :329                                 */
ng_unname(node);                 /* :330                                 */
IFP2NG(ifp) = NULL;              /* :331  (4)                            */
priv = node->private;            /* :332                                 */
bzero(priv, sizeof(*priv));      /* :333  (6)                            */
kfree(priv, M_NETGRAPH);         /* :334  (7) priv FREED                 */
node->private = NULL;            /* :335  (8)                            */
ng_unref(node);                  /* :336                                 */
```

The readers run in arbitrary thread / netisr context and take no lock, no
reference, and check no flag:

- `ng_ether_input`        :206-213  `node = IFP2NG(ifp); priv = node->private; if (priv->lower ...)` (RX path, if_ethersubr)
- `ng_ether_input_orphan` :222-230  same (RX orphan path)
- `ng_ether_output`       :260-273  same (TX path, if_ethersubr)
- `ng_ether_rcvdata`      :505-513  `priv = hook->node->private;` (netgraph data path)
- `ng_ether_rcvmsg`       :401-497  `priv = node->private;` (control path; guarded only by the caller's `NG_INVALID` check at netgraph/ng_base.c:2059 — itself a TOCTOU)

A reader that loads `node` (or `hook->node`) before step (4)/(8) and then
loads `->private` after (8) dereferences NULL (deterministic panic); a reader
that lands between (7) and (8) dereferences **freed** memory (true UAF read of
`priv->lower`/`priv->upper`/`priv->ifp` — heap contents controlled by whatever
reuses the M_NETGRAPH slab block). The v1 core's only incidental protection
is the `HK_INVALID` flag test in `ng_send_data()`/`ng_send_dataq()`
(netgraph/ng_base.c:1684, 1711) — which (a) does not cover the three
if_ethersubr callbacks at all, and (b) is itself a check-then-use TOCTOU
(`hook->flags` tested, then `hook->peer->node->type->rcvdataq` dereferenced,
racing `ng_destroy_hook()`'s `hook->peer = NULL` at ng_base.c:822-823).

This is the v1 twin of DF-3529 (ng7 `netgraph7/ether/ng_ether.c`, filed as
speculative-Low). Note the v1 file is *worse* positioned than ng7 in one
respect: ng7-on-DragonFly funnels every item through the cpu0 netgraph port
(netgraph7/netgraph/ng_base.c:1948-1956 `lwkt_sendmsg(ng_cpuport(0), ...)`)
which serializes rcvdata/rcvmsg against teardown; **v1 has no such funnel**
(RX/TX callbacks are entered directly from if_ethersubr in per-packet
context, and items are dispatched from per-CPU netisr holding only the MP
lock, ng_base.c:2034).

Distinct from known findings on this file:
- DF-2587 is the `node == NULL` deref (`IFP2NG` NULL) — here the **node is
  valid and `node->private` is the freed/NULL'd object**.
- DF-0629 is the missing `bridge_input_p` handoff in `rcv_upper` (a filter
  bypass, not memory safety).

## Why the lead "DF-0617 twin" is dead

DF-0617 (ng7) is a UAF caused by `rcv_upper` *calling* `bridge_input_p()`,
discarding its return value (an mbuf that bridge_input_p may have freed), and
feeding the freed mbuf to `ether_demux_oncpu`. The v1 `ng_ether_rcv_upper`
(:566-587) **contains no `bridge_input_p` call at all** — it goes straight to
`ether_demux(m)` — so the freed-mbuf-relay primitive cannot exist here; the
absence of the bridge handoff is exactly already-known DF-0629. Verified by
reading the whole function; no return value is discarded because no such call
is made.

## Reproduction

### Natural race (honest attempt)

`race_poc.c`: 4 worker processes pinned to different CPUs, each looping
`ifconfig tapN create` → open `/dev/tapN` → burst-write 128 RX frames (each
write == one `ether_input` → `ng_ether_input` on that ifp) → close → destroy.
120 seconds: **20,339 cycles, ~2.6M frames, no panic.** The winning
interleaving needs a reader descheduled *between two adjacent loads*
(`IFP2NG(ifp)` then `node->private`) spanning the whole (4)..(8) teardown —
a ~2 ns reader window vs a microsecond-plus teardown — so a bounded run not
hitting it is expected and documented (same experience as DF-2587's natural
chase). This bounds the *practical* severity at Low.

### Deterministic primitive confirmation (harness)

`harness_mod.c` loads after `kldload ng_ether` and puts vtnet0's live ng_ether
node into the exact post-teardown observable state — `node` left valid (as an
in-flight reader that loaded it before step (4) would hold), `node->private`
set to NULL (its value after step (8)). The *very next packet* on vtnet0
(QEMU user-net background RX traffic) panicked the stock kernel:

```
DF3530: vtnet0 ng_ether node=0xfffff8011728d4a0 private=0xfffff8008d721a98 -> NULL.
Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0x10
Stopped at      ng_ether_input+0xb:     movq    0x10(%rax),%rdi
```

`0x10` = offset of `lower` in `struct private`; `%rax` = `priv` = NULL; the
faulting instruction is the `priv->lower` load at ng_ether.c:210. Current
process `Idle` (netisr RX context) — exactly the reader the finding describes.
This signature is distinct from DF-2587's (`ng_ether_output`, VA 0x28, node
NULL) and proves the missing-priv-lifetime-check primitive end to end. In the
(7)→(8) sliver the same reader gets a freed-heap `priv` (UAF read) rather
than NULL — not crash-deterministic, hence not the harness variant, but the
source ordering (:334 kfree before :335 NULL) makes it real.

## Exploit chain assessment

Impact ceiling here is kernel memory-safety violation from a root-gated race:
trigger requires interface detach (ifconfig destroy / driver unload / device
hotplug) racing live traffic on that interface. Practical payoff demonstrated:
panic. Theoretical payoff: `priv` is freed while readers still deref
`priv->ifp`, `priv->upper/lower` (pointer-chasing into further calls —
`ether_output_frame(priv->ifp, m)`) — heap-grooming the M_NETGRAPH slab block
could steer those, but the nanosecond window makes this implausible even for
root; not pursued further. No uid0 chain claimed.

## Fix validation

`fix.diff` adds NULL guards (node and private) to the four reachable readers
(`ng_ether_input`, `ng_ether_input_orphan`, `ng_ether_output`,
`ng_ether_rcvdata`); semantics: no hooks observable ⇒ packet continues
normally / data dropped with ENXIO. Applied to the guest's pristine
`/usr/src` (4/4 hunks), `make` in `sys/netgraph/ether` clean under `-Werror`,
installed, `kldload ng_ether` + SAME harness state + `ping -c3 10.0.2.2`:
**3/3 packets, 0% loss, no panic**, harness unload restores the node,
post-restore ping OK. Baseline panic gone ⇒ fix_status = fixed. Guest reset
clean afterwards (source sha and stock module sha verified back to pristine).

## Kernel references

- sys/netgraph/ether/ng_ether.c:204-213 (ng_ether_input reader)
- sys/netgraph/ether/ng_ether.c:219-233 (ng_ether_input_orphan reader)
- sys/netgraph/ether/ng_ether.c:257-273 (ng_ether_output reader)
- sys/netgraph/ether/ng_ether.c:502-513 (ng_ether_rcvdata reader)
- sys/netgraph/ether/ng_ether.c:321-337 (ng_ether_detach: unsynchronized free)
- sys/netgraph/netgraph/ng_base.c:1711-1714 (HK_INVALID check-then-use TOCTOU)
- sys/netgraph/netgraph/ng_base.c:2059-2065 (rcvmsg NG_INVALID TOCTOU)
- sys/netgraph7/netgraph/ng_base.c:1948-1956 (contrast: ng7 cpu0 funnel that v1 lacks)

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