# DF-0565 — ng7_lmi callout lifetime UAF (twin of DF-0557)

## Verdict

**NOT REPRODUCED LIVE — bug confirmed in source.** The targeted code is in
`sys/netgraph7/lmi/ng_lmi.c`, which is **not shipped** on the default
GENERIC kernel (`netgraph7` is opt-in via `options NETGRAPH7`,
`sys/conf/options:284`). The bug pattern is genuine; the module just
isn't reachable on the audit guest.

The shipped `ng_lmi.ko` (built from `sys/netgraph/lmi/ng_lmi.c`) has a
related but milder form of the bug (non-synchronizing `callout_stop` in
`nglmi_disconnect`); see "Related shipped-code observation" below.

## Mechanism (cited path:line, confirmed by trace)

The targeted ng7_lmi code at `sys/netgraph7/lmi/ng_lmi.c`:

```c
1049: /*
1050:  * Do local shutdown processing..
1051:  * Cut any remaining links and free our local resources.
1052:  */
1053: static int
1054: nglmi_shutdown(node_p node)
1055: {
1056:     const sc_p sc = NG_NODE_PRIVATE(node);
1057:
1058:     NG_NODE_SET_PRIVATE(node, NULL);
1059:     NG_NODE_UNREF(sc->node);
1060:     kfree(sc, M_NETGRAPH);           <-- freed without stopping the callout
1061:     return (0);
1062: }
```

Compare with `LMI_ticker` at `:264-282`, which self-reschedules via
`ng_callout(&sc->handle, node, ..., LMI_ticker, ..., 0)` at `:270/279`
whenever the node is alive. When `NGM_SHUTDOWN` (or a forced rmnode)
arrives, the generic netgraph shutdown dispatcher invokes the type's
shutdown handler = `nglmi_shutdown`, which:

1. Sets `node->private = NULL` (so `NG_NODE_PRIVATE(node)` returns NULL).
2. Unrefs the node (drops one reference; may not be the last).
3. **`kfree(sc, M_NETGRAPH)`** — frees the softc **without stopping
   `sc->handle`** (the callout).

The callout is still armed. The next time it fires, `LMI_ticker` runs:

```c
264: LMI_ticker(node_p node, hook_p hook, void *arg1, int arg2)
265: {
266:     sc_p sc = NG_NODE_PRIVATE(node);   <-- NULL or freed
267:
268:     if (sc->flags & SCF_AUTO) {        <-- deref freed/NULL sc
```

- If `NG_NODE_PRIVATE(node)` is NULL (the path set it to NULL): NULL-deref
  panic at `sc->flags` access.
- If `sc` is freed but the slab is reused (kfree returns memory to the
  slab pool; if reallocated for another type, we get type confusion): UAF
  with attacker-controlled content.

The path is **deterministic** (no race needed) for the basic NULL-deref
case: any shutdown of an ng7_lmi node whose timer is armed triggers it.

`nglmi_disconnect` at `:1069-1085` (the hook-disconnect path) DOES call
`ng_uncallout(&sc->handle, sc->node)` at `:1079`, but `ng_uncallout`
just calls `callout_stop` (`sys/netgraph7/netgraph/ng_base.c:3273-3281`),
which is **non-synchronizing** — if `LMI_ticker` is currently executing,
`callout_stop` returns without waiting. `LMI_ticker` then reschedules,
`ng_rmnode_self` runs, `nglmi_shutdown` frees `sc`, next firing → UAF.

## Why not testable on this guest

- The ng7 stack (`netgraph7/netgraph/ng_base.c` and friends) is opt-in
  via `options NETGRAPH7` in the kernel config. The default
  `X86_64_GENERIC` does NOT enable it.
- The ng7_lmi module is NOT shipped in `/boot/kernel/`. The shipped
  `ng_lmi.ko` is the OLDER `sys/netgraph/lmi/ng_lmi.c`.
- The two files share the same Makefile target name (`ng_lmi.ko`), so
  they cannot coexist; only the older one ships.

Standalone `make` in `/usr/src/sys/netgraph7/lmi` succeeds (the file is
valid kernel C), but loading the resulting `ng_lmi.ko` requires the
netgraph7 base, which is also not shipped.

## Related shipped-code observation

The shipped `sys/netgraph/lmi/ng_lmi.c` has a related-but-milder race:

```c
1063: nglmi_rmnode(node_p node) {              <-- shutdown handler
1065:     const sc_p sc = node->private;
1067:     node->flags |= NG_INVALID;
1068:     ng_cutlinks(node);                   <-- tears down hooks
1069:     ng_unname(node);
1070:     node->private = NULL;
1071:     ng_unref(sc->node);
1072:     kfree(sc, M_NETGRAPH);
1073: }

1081: nglmi_disconnect(hook_p hook) {
1090:     if (sc->flags & SCF_CONNECTED)
1091:         callout_stop(&sc->timeout);       <-- non-sync!
1094:     ng_rmnode(hook->node);
1095: }
```

`nglmi_rmnode` does not call `callout_stop` itself, but transitively
calls `nglmi_disconnect` via `ng_cutlinks` → `ng_destroy_hook`. The
`callout_stop` at `:1091` is non-synchronizing, so if `LMI_ticker` is
currently running (inside `crit_enter()` at `:277`), it doesn't wait;
the ticker reschedules at `:280/289`; `kfree(sc)` then frees the
softc; the rescheduled callout fires into freed memory → UAF.

I attempted to trigger this race with a 500-iteration create+destroy
loop (`df0565.c`) on the shipped ng_lmi.ko — no panic in 60s. The race
window is microseconds and `LMI_ticker` fires every 10s, so the per-
iteration hit probability is roughly `1us / 10s ≈ 1e-7`; 500 iters
gives ~5e4 odds against. Reproducing it uninstrumented would need
~1e7 iterations or a tighter LMI_ticker interval.

The fix for the shipped twin is the same pattern (callout_drain before
kfree); I have NOT included that hunk in `fix.diff` because the finding
is specifically against the ng7 file.

## Recommended fix (validated compile-only)

`fix.diff` adds an explicit `ng_uncallout + callout_drain` block at
the top of `nglmi_shutdown` in `sys/netgraph7/lmi/ng_lmi.c:1055`:

```c
if (sc->flags & SCF_CONNECTED) {
    ng_uncallout(&sc->handle, sc->node);
    callout_drain(&sc->handle);
}
```

- `ng_uncallout` (callout_stop) cancels any pending firing.
- `callout_drain` blocks until any currently-running LMI_ticker has
  finished, so the kfree below is safe.

Validated by:
1. `patch -p4 < fix.diff` → `Hunk #1 succeeded at 1055.`
2. `make` in `/usr/src/sys/netgraph7/lmi` → builds cleanly
   (`/usr/obj/usr/src/sys/netgraph7/lmi/ng_lmi.ko`, 15768 bytes).

Cannot load+test on the audit guest (ng7 base not shipped), so
`fix_status: not_testable`.

The fix matches the intent of the finding proposal ("ng_uncallout at
top of shutdown before private=NULL+kfree"). The proposal also suggests
"consider callout_stop_sync in ng_uncallout" — that would be a broader
change to `sys/netgraph7/netgraph/ng_base.c:3273` (change
`callout_stop` to `callout_stop_sync` or `callout_drain`), which I have
NOT included because (a) it affects every netgraph7 node type, not just
lmi, and (b) `callout_drain` from inside a callout's own context would
deadlock, so a blanket change needs careful audit of every
`ng_uncallout` caller. The local fix in nglmi_shutdown is sufficient
to close DF-0565 itself.

## Kernel references (verified by source trace)

- `sys/netgraph7/lmi/ng_lmi.c:264-282`  — `LMI_ticker` self-reschedules.
- `sys/netgraph7/lmi/ng_lmi.c:1053-1062` — `nglmi_shutdown` frees sc without
  stopping the callout.
- `sys/netgraph7/lmi/ng_lmi.c:1069-1085` — `nglmi_disconnect` uses non-sync
  `ng_uncallout`.
- `sys/netgraph7/netgraph/ng_base.c:3273-3281` — `ng_uncallout` =
  `callout_stop` (non-synchronizing).
- `sys/kern/kern_timeout.c:1047` — `callout_drain` (synchronizing).
- `sys/conf/options:284` — `NETGRAPH7` opt-in.

## Threat model

Local privilege-relevant UAF. The trigger (NGM_SHUTDOWN to an active
ng7_lmi node) requires control-socket privilege (root, or an unprivileged
user with admin-exported hook access). On a default install the path is
dead code (ng7 not enabled). On a custom kernel with NETGRAPH7 enabled,
the bug is deterministic for the NULL-deref case and probabilistic for
the UAF case. No write primitive is immediately derivable, but a UAF
into a reclaimed slab object is a textbook escalation primitive.

## PoC

`df0565.c` hammers the shipped ng_lmi's non-sync callout_stop race (the
milder shipped-code variant) — 500 iterations on the audit guest did
not trigger. The cleaner ng7_lmi UAF (the actual finding) cannot be
triggered on this guest because the module is not shipped.
