# DF-0676 — ng_one2many XMIT_ALL double-free / use-after-free

## Verdict: REPRODUCED (double-free + UAF primitive confirmed; fix validated)

In `ng_one2many_rcvdata()`'s `NG_ONE2MANY_XMIT_ALL` branch, the mbuf is only
**peeked** from the item (`m = NGI_M(item)`, comment "just peaking"), then in the
`m_dup()`-failure error path it is freed by hand:

```c
/* sys/netgraph7/one2many/ng_one2many.c:425,464-468 */
m = NGI_M(item);                 /* PEEK: mbuf still owned by item */
...
m2 = m_dup(m, M_NOWAIT);
if (m2 == NULL) {
    mdst->stats.memoryFailures++;
    NG_FREE_ITEM(item);          /* el_flags |= NGQF_FREE; does NOT clear _NGI_M(item) */
    NG_FREE_M(m);                /* m_freem(m); _NGI_M(item) still == freed mbuf */
    return (ENOBUFS);
}
```

`NG_FREE_ITEM` (`netgraph7/netgraph.h:816`) sets `NGQF_FREE` but **does not null
`_NGI_M(item)`**. Control returns `ENOBUFS` to `ng_apply_item()`
(`netgraph7/netgraph/ng_base.c:2027→2109`), which calls `ng_unref_item(item, error)`
→ `ng_free_item(item)` (`ng_base.c:2612`). The `NGQF_DATA` case of `ng_free_item`
does `NG_FREE_M(_NGI_M(item))` (`ng_base.c:2635`) — freeing the **already-freed mbuf**
→ double-free.

## Primitive characterization (measured on this guest)

The harness `ng_o2m_df.c` replays the exact macro sequence from `netgraph7/netgraph.h`
on a real mbuf + a real `struct ng_item`:

```
baseline (buggy):   _NGI_M(item)=0xfffff801186e3800 (DANGLING -> freed mbuf)
  -> ng_free_item frees it again: DOUBLE FREE (silent, objcache has no trap)
  -> 16 fresh m_gethdr: address 0xfffff801186e3800 returned 2 time(s)  => ALIASING / UAF
```

| property | value |
|---|---|
| object double-freed | an `mbuf` (objcache-backed) |
| detection | **none** on GENERIC — the mbuf objcache has no double-free trap, so the corruption is silent |
| derived primitive | the freed mbuf is handed out to **two** subsequent `m_gethdr()` callers → they alias the same memory → writes to one corrupt the other → **use-after-free** |

This is a **silent** double-free → UAF: far more dangerous than a panicking one.

## Reachability — ROOT-ONLY (valid hard blocker for unpriv uid=0)

Building the one2many topology (creating nodes/hooks) needs the `NG_CONTROL` socket,
whose `ngc_attach()` (`sys/netgraph7/socket/ng_socket.c:182`) does
`caps_priv_check(SYSCAP_RESTRICTEDROOT)` → **root-only**. An unprivileged user cannot
create the netgraph nodes, so the double-free is not reachable without root. The live
trigger additionally requires `m_dup(M_NOWAIT)` to fail (mbuf pressure), which is
environment-dependent; the harness removes that dependency by replaying the exact
error-path macros deterministically.

## Escalation ceiling

The primitive is a UAF on a kernel mbuf: after the double-free, an attacker who can
reclaim the aliased mbuf (via netgraph traffic) and shape its content can corrupt a
victim object that shares the mbuf bucket. Given no SMAP/SMEP/KASLR, corrupting a
function-pointer-bearing object reachable from the mbuf bucket is a path to kernel code
execution. However, the whole chain requires **root** to build the topology and to drive
`m_dup` failure, so this is a **root→kernel** primitive (root→kernel is already
game-over); there is no unprivileged→uid=0 path because the privilege boundary
(NG_CONTROL socket) is not crossed.

## PoC changes

- `ng_o2m_df.c` + `Makefile`: harness that builds a real mbuf + `struct ng_item`,
  replays the XMIT_ALL error-path macros + `ng_free_item`'s NGQF_DATA case, and
  detects the double-free via free-list aliasing. `hw.df0676.apply_fix` loader tunable
  selects baseline (0, double-free) vs the fix (1, `NGI_GET_M` detach, no double-free).

## Fix (fix.diff)

Detach the mbuf from the item before freeing it in the error path:

```c
if (m2 == NULL) {
    mdst->stats.memoryFailures++;
    NGI_GET_M(item, m);     /* m = _NGI_M(item); _NGI_M(item) = NULL */
    NG_FREE_ITEM(item);
    NG_FREE_M(m);           /* freed exactly once; ng_free_item sees _NGI_M=NULL (no-op) */
    return (ENOBUFS);
}
```

`NGI_GET_M` (`netgraph.h:825`) NULLs `_NGI_M(item)`, so `ng_free_item`'s
`NG_FREE_M(_NGI_M(item))` becomes a no-op — eliminating the double free. Validated:
harness aliasing count drops **2 → 0** with the fix.

## Kernel references (verified)
- `sys/netgraph7/one2many/ng_one2many.c:425` — `m = NGI_M(item)` peek
- `sys/netgraph7/one2many/ng_one2many.c:464-468` — the double-free error path
- `sys/netgraph7/netgraph.h:816-820` — `NG_FREE_ITEM` (does NOT clear `_NGI_M`)
- `sys/netgraph7/netgraph.h:825-828` — `NGI_GET_M` (the fix primitive)
- `sys/netgraph7/netgraph.h:919-924` — `NG_FREE_M`
- `sys/netgraph7/netgraph/ng_base.c:2027,2109` — `ng_apply_item` → `ng_unref_item`
- `sys/netgraph7/netgraph/ng_base.c:2612,2632-2635` — `ng_free_item` NGQF_DATA double-free
- `sys/netgraph7/socket/ng_socket.c:182` — `ngc_attach` root-only (`SYSCAP_RESTRICTEDROOT`)
