# DF-3492 — v1 pppoe_ticker races ng_pppoe_disconnect freeing the session
(Low / base:net — structural analysis, not QEMU-verified; twin of ng7 DF-3488)

File: `sys/netgraph/pppoe/ng_pppoe.c`

## Mechanism

`pppoe_ticker()` (:1510-1555) executes from the callout subsystem and
dereferences unconditionally, before any state check:

```c
hook_p hook = arg;
sessp  sp = hook->private;    /* :1514  NULL after disconnect */
negp   neg = sp->neg;         /* :1515  deref of sp */
```

`ng_pppoe_disconnect()` (:1418-1505) tears a session down in a
different thread context:

```c
if (sp->neg) {
        callout_stop(&sp->neg->timeout_ch);   /* :1490 */
        ...
        kfree(sp->neg, M_NETGRAPH);
}
kfree(sp, M_NETGRAPH);                        /* :1495 */
hook->private = NULL;                         /* :1496 */
```

`callout_stop()` cannot recall a callout whose handler has already been
entered on another CPU (DFly callout semantics), and there is no
reference/handoff protecting `hook->private` across the two contexts. A
ticker that fired (or is executing) while the hook is being destroyed —
e.g. a remote PADT arriving during the 2-64 s retransmit backoff window
(`ng_pppoe_rcvdata` PADT case :1186-1204 → `ng_destroy_hook`), or
ethernet-hook loss / node teardown — reads `hook->private` after
`:1496` (NULL deref → panic) or uses `sp`/`sp->neg` after `kfree`
(:1495/:1493) (use-after-free read → panic or worse).

Additional member of the same family: a PADS/PADS-complete transition
frees `sp->neg` (:1181-1183) while a previously-armed ticker may still
execute `sp->neg->m` in the SINIT/SREQ case (:1532).

## Why not QEMU-verified

The window is the tick-vs-teardown race on a 2-64 s timer — the ng7
twin DF-3488 was likewise filed from structural analysis (Low,
race-timing). A reliable trigger needs either a callout hammer (custom
module) or many thousands of connect/PADT cycles; not worth destabilizing
the single-tenant guest for a Low-severity twin whose mechanism is
source-identical to the ng7 finding.

## Fix (as ng7 DF-3488)

Re-check `sp = hook->private; if (sp == NULL) return;` under crit at
ticker entry, and/or convert teardown to `callout_stop_sync()` /
serialize hook destruction with the callout.
