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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3488 Β· 7 files
FileTypeDescriptionSize
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)

VERDICT.md
↓ download raw

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

Evidence

pppoerace7.c, run.log (PPPOERACE7-DONE 100 cycles, no panic), harness kernel boot

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_testable
↓ fix.diffper-fix-DF-3488

Confirmed 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.