β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3492

pppoe_ticker dereferences hook->private/sp->neg unconditionally and races ng_pppoe_disconnect freeing the session β€” NULL-deref/UAF panic (DF-3488 twin)

Summary

pppoe_ticker runs from the callout subsystem and reads sp=hook->private, neg=sp->neg before any state check, while ng_pppoe_disconnect stops the callout, frees sp->neg and sp and clears hook->private in the tearing thread. callout_stop cannot recall an already-entered callout on another CPU; a ticker firing during hook teardown (remote PADT during the 2-64s retransmit backoff) reads NULL or freed memory -> panic. Same family: PADS completion frees sp->neg while an armed SINIT/SREQ ticker still dereferences sp->neg->m. Structural (Low, race-timing; analysis in pack README). Fix: guard ticker entry + synchronize teardown with the callout before freeing.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3492 Β· 3 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
manifest.json β€” 648 B view raw
verdict.json β€” 2.0 KB view raw

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:

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:

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.

Fix verification

not_testable
per-fix-DF-3492

Confirmed kernel references

Detail

Evidence (decisive lines)

README.md (line-cited structural analysis); compare ng7 DF-3488 evidence pack

Verified recommended fix

Guard ticker entry: sp = hook->private; if (sp == NULL) return; under crit_enter(), and synchronize teardown with the callout (callout_stop_sync or equivalent) before freeing sp/neg

Verdict

Structural analysis only (same discipline as the ng7 twin DF-3488): pppoe_ticker() dereferences hook->private and sp->neg at :1514-1515 before any state check, while ng_pppoe_disconnect() stops the callout (:1490), frees sp->neg and sp (:1493,:1495) and clears hook->private (:1496) from the tearing thread. callout_stop() cannot recall an already-entered callout on another CPU, so a ticker firing concurrently with hook teardown (remote PADT during the 2-64s retransmit window, or ethernet-hook loss) reads NULL (guaranteed panic) or freed memory (UAF). Not QEMU-verified: Low-severity race requiring a callout hammer to hit reliably; mechanism is source-identical to the already-filed ng7 finding.