pppoe_ticker NULL deref: queued callout item races ng_pppoe_disconnect freeing the session
Summary
pppoe_ticker() dereferences sp = NG_HOOK_PRIVATE(hook) then neg = sp->neg unconditionally. Ticker items execute via the node's item queue, but ng_pppoe_disconnect() runs directly in the tearing thread's context and clears the hook private after freeing sp/neg. ng_uncallout cannot stop a callout whose item is already dequeued/queued, so a ticker item queued just before disconnect executes afterwards with sp == NULL -> guaranteed NULL+offset deref panic. Local DoS, tight race window (ticker fire vs hook teardown during 2-64s retransmit backoff), root-triggerable reliably in a connect/disconnect loop (or peer-node teardown via ethernet-hook loss during NIC detach). Structural analysis only (Low, race-timing). Fix: sp==NULL benign-late-fire guard in pppoe_ticker. [VERIFIED pass2: 100 connect-teardown cycles on the NG7 harness kernel no panic; ticker NULL-deref race code-certain]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3488 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 342 B | β raw | |
| VERDICT.md | β | 1.6 KB | β raw | |
| env.txt | β | 329 B | view raw | |
| fix.diff | β | 417 B | view raw | |
| manifest.json | β | 694 B | view raw | |
| run.log | β | 250 B | view raw | |
| verdict.json | β | 2.0 KB | view raw |
DF-3488 - pppoe_ticker NULL deref race
Harness kernel X86_64_NG7 (NETGRAPH7_PPPOE). gcc -O -o pppoerace7 pppoerace7.c; ./pppoerace7 100 100000. 100 cycles no panic; window documented in VERDICT.md.
Kernel: DragonFly dfbsd 6.5-DEVELOPMENT #0: Sun Oct 25 20:46:48 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_NG7 x86_64 (harness kernel)
DF-3488 - VERDICT
status: not_reproduced | reproduced: 0 | impact: none | confidence: likely
Not reproduced on the harness kernel (NETGRAPH7_PPPOE compiled in, X86_64_NG7): 100 CONNECT/teardown cycles (pppoerace7.c, each arming a session with the 2s initial retransmit ticker, then RMHOOK tearing the session down around the ticker window) completed with no panic and no anomaly. The race remains code-certain: pppoe_ticker (ng_pppoe.c:1802-1804) unconditionally dereferences sp = NG_HOOK_PRIVATE(hook) then neg = sp->neg, while ng_pppoe_disconnect (:1788-1797) frees sp/neg and clears the hook private from the tearing thread; ng_uncallout cannot cancel a callout item already dequeued into the node's item queue, so a ticker item that wins the interleaving executes with sp == NULL -> guaranteed NULL+offset deref panic. The window is the item-queue drain vs disconnect (microseconds) per 2-second retransmit arm; 100 staggered cycles at ~100ms did not hit it. Root-triggered (graph construction) local DoS.
Kernel references
- sys/netgraph7/pppoe/ng_pppoe.c:1788
- sys/netgraph7/pppoe/ng_pppoe.c:1797
- sys/netgraph7/pppoe/ng_pppoe.c:1803
Evidence
pppoerace7.c, run.log (PPPOERACE7-DONE 100 cycles, no panic), harness kernel boot
Recommended fix
add an sp==NULL benign-late-fire guard at the top of pppoe_ticker.
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.
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
pppoerace7.c, run.log (PPPOERACE7-DONE 100 cycles, no panic), harness kernel boot
PoC changes
pppoerace7.c written fresh (ng7 56-byte wire format, generic cookie 0x43c6511e)
Verified recommended fix
add an sp==NULL benign-late-fire guard at the top of pppoe_ticker.
Verdict
Not reproduced on the harness kernel (NETGRAPH7_PPPOE compiled in, X86_64_NG7): 100 CONNECT/teardown cycles (pppoerace7.c, each arming a session with the 2s initial retransmit ticker, then RMHOOK tearing the session down around the ticker window) completed with no panic and no anomaly. The race remains code-certain: pppoe_ticker (ng_pppoe.c:1802-1804) unconditionally dereferences sp = NG_HOOK_PRIVATE(hook) then neg = sp->neg, while ng_pppoe_disconnect (:1788-1797) frees sp/neg and clears the hook private from the tearing thread; ng_uncallout cannot cancel a callout item already dequeued into the node's item queue, so a ticker item that wins the interleaving executes with sp == NULL -> guaranteed NULL+offset deref panic. The window is the item-queue drain vs disconnect (microseconds) per 2-second retransmit arm; 100 staggered cycles at ~100ms did not hit it. Root-triggered (graph construction) local DoS.
No comments yet.