nfe_stop() deadlocks the callout subsystem against nfe_tick (AB-BA): synchronous callout_stop() issued while holding the driver serializer vs a tick callback parked in lwkt_serialize_enter β mechanism PROVEN on the guest via nfe-shaped replica (DF-3715/4025/4046 family instance)
Summary
nfe_attach arms one NON-MP callout; nfe_tick BLOCKS acquiring the driver serializer, then re-arms itself UNCONDITIONALLY while holding it (no IFF_RUNNING gate). nfe_stop issues the synchronous callout_stop as its FIRST action after ASSERT_SERIALIZED - while HOLDING the serializer. A tick dequeued and parked in lwkt_serialize_enter deadlocks against the stopper: both threads wedge permanently, the serializer is never released, one CPU's softclock stops delivering ALL callouts. All stop callers hold the serializer (detach/shutdown/suspend/ioctl/init from MTU/cap/watchdog). Mechanism PROVEN live with nfereplica.ko (stopper D2/costp vs softclock/slize; console ends at the AB-BA marker; guest could not shut down; force-reset). lwkt_serialize_try + retry fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4051 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.1 KB | β raw | |
| VERDICT.md | β | 4.2 KB | β raw | |
| nfereplica.c | β | 5.8 KB | view raw | |
| Makefile | β | 134 B | β download | |
| build.sh | β | 82 B | view raw | |
| run.sh | β | 283 B | view raw | |
| build.log | β | 5.7 KB | view raw | |
| run.log | β | 2.8 KB | view raw | |
| serial_console.txt | β | 268 B | view raw | |
| env.txt | β | 278 B | view raw | |
| fix.diff | β | 895 B | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 3.6 KB | view raw |
DF-4051 β nfe_stop() vs nfe_tick() callout AB-BA deadlock (nfe(4) instance of the DF-3715/4025/4046 family)
What
nfe_attacharms one callout with plain non-MPcallout_init(&sc->sc_tick_ch)(if_nfe.c:636).nfe_tick(if_nfe.c:2181-2193) blocks acquiring the driver serializer (lwkt_serialize_enter,:2187), doesmii_tick, then re-arms itself UNCONDITIONALLY while holding it (callout_reset(&sc->sc_tick_ch, hz, nfe_tick, sc),:2190β no IFF_RUNNING gate).nfe_stop(if_nfe.c:1585-1640) issues the synchronouscallout_stop(&sc->sc_tick_ch)(:1593) as its first action afterASSERT_SERIALIZED(:1591) β i.e. while holding that serializer.- DragonFly
callout_stop()is synchronous (sys/kern/kern_timeout.c:1091-1093β_callout_cancel_or_stop(..., sync=1); thessleep "costp"wait atkern_timeout.c:920): it waits for an INPROG callback to finish.
A nfe_tick dequeued by a softclock thread and parked in
lwkt_serialize_enter therefore deadlocks against the stopper: both threads
wedge permanently, the interface serializer is never released (every later
ioctl / if_start on nfeX blocks), and the affected softclock stops delivering
all callouts on that CPU.
Reachable from every down/detach/reinit path racing the 1 Hz tick:
nfe_detach :669-672, nfe_shutdown :705-708, nfe_suspend :716-719,
nfe_ioctl SIOCSIFFLAGS :1025-1027, and nfe_init :1446 (nfe_stop is its
first statement) β itself reachable from SIOCSIFMTU :1005-1007,
SIOCSIFCAP :1049-1051, nfe_resume :730-732, nfe_watchdog :1431.
Reproduce (guest, root)
scp -F dfbsd-qemu/config nfereplica.c Makefile dfbsd:/root/nfereplica/ ssh -F dfbsd-qemu/config dfbsd 'cd /root/nfereplica && make' kldload /root/nfereplica/nfereplica.ko sysctl kern.nfereplica.arm=1 # arm the nfe_tick replica (1-tick period) sleep 2 (sysctl kern.nfereplica.stop=1 &) # nfe_stop shape: NEVER returns sleep 6; dmesg | grep nfereplica | tail -4 ps -o pid,stat,wchan,command -ax | grep -E "stop=1|softclock" | grep -v grep
Expected output (observed, run.log + serial_console.txt)
nfereplica: nfe_tick parked on our serializer (fired=201 ran=200) - entering callout_stop()
nfereplica: IF THIS IS THE LAST nfereplica LINE, AB-BA DEADLOCK IS PROVEN
...
974 D2 costp sysctl kern.nfereplica.stop=1 <- kern_timeout.c:920
-1 B3 slize (softclock 3) <- lwkt_serialize_enter
The stopper sits in costp across β₯2 samples minutes apart (permanent), the
console ends at the marker line, and the guest cannot shut down cleanly β
vm.sh reset with-src is the documented recovery.
Hardware gating / family convention
The guest has no NVIDIA nForce MCP NIC (pciconf -l shows none; QEMU
does not emulate forcedeth), so the in-driver instance cannot be triggered at
runtime; per the DF-4025/DF-4046 family convention the persisted status is
inconclusive with the mechanism PROVEN by this deterministic replica of
if_nfe.c's exact callout/serializer protocol.
DF-4051 VERDICT β nfe_stop() vs nfe_tick() callout AB-BA (mechanism PROVEN on guest; nfe runtime hardware-gated)
Reproduced?
Mechanism: PROVEN on the guest kernel by a deterministic replica
(nfereplica.ko) of if_nfe.c's exact callout/serializer protocol β plain
non-MP callout_init (if_nfe.c:636), tick handler that BLOCKS in
lwkt_serialize_enter (:2187) and re-arms itself unconditionally while
holding the serializer (:2190), and a stop that issues the synchronous
callout_stop() under the serializer as its first action (:1591-:1593).
nfe runtime: hardware-gated (guest has no nForce MCP NIC, env.txt;
QEMU does not emulate forcedeth), so per family convention (DF-4025,
DF-4046) the persisted status is inconclusive.
Observed on the guest (run.log + serial_console.txt)
- Module built:
BUILD_RC=0(build.log, -Werror clean). - Armed; the tick fired 201 times, ran 200 (re-arm loop working, period
1 tick vs the real driver's
hz). - nfe_stop-shaped stop under the serializer: the 201st firing parked in
lwkt_serialize_enter; console ends atIF THIS IS THE LAST nfereplica LINE, AB-BA DEADLOCK IS PROVENβcallout_stop()never returned. - Cross-CPU AB-BA visible in
ps: stopper pid 974D2wchancostp(ssleep(c, &c->spin, 0, "costp", 0),sys/kern/kern_timeout.c:920, reached fromcallout_stopβ_callout_cancel_or_stop(..., sync=1)atkern_timeout.c:1091-1093) vs(softclock 3)blocked at wchanslize(lwkt_serialize_enter) β each holds what the other needs, forever. Two samples minutes apart show the identical pair (permanent). - An early replica revision (v1, tsleep-based park-wait) wedged even
harder: the stopper's tsleep timeout is delivered by the same softclock
the tick parks, so the stopper starved in its own wait (
D4/nfetkwtwhilesoftclock 4/slice) β additional evidence of the CPU-wide callout starvation this protocol causes. v2 (DELAY-based park-wait, shipped here) isolates the true block point atcallout_stop()/'costp'. - The observing ssh session's stream froze mid-script (its shell lived on
the wedged CPU's timers); the guest could not shut down cleanly β
vm.sh reset with-srcrecovered it (healthy afterwards,env.txt).
Root cause (path:line)
if_nfe.c:636βcallout_init(&sc->sc_tick_ch)(non-MP variant).if_nfe.c:2181-2193βnfe_tick:lwkt_serialize_enterat:2187(BLOCKING acquire),mii_tick:2189, then unconditionalcallout_reset(&sc->sc_tick_ch, hz, nfe_tick, sc)at:2190under the serializer (no IFF_RUNNING gate), exit at:2192.if_nfe.c:1585-1640βnfe_stop:ASSERT_SERIALIZED:1591thencallout_stop(&sc->sc_tick_ch):1593as the FIRST teardown action while holding the serializer.- Callers of nfe_stop all hold the serializer:
nfe_detach :669-672,nfe_shutdown :705-708,nfe_suspend :716-719,nfe_ioctlSIOCSIFFLAGS:1025-1027,nfe_init :1446(first statement; reachable from SIOCSIFMTU:1005-1007, SIOCSIFCAP:1049-1051,nfe_resume :730-732,nfe_watchdog :1431). sys/kern/kern_timeout.c:1091-1093β synchronouscallout_stop(_callout_cancel_or_stop(..., sync=1)),:920β thecostpwait.
Threat
Local DoS: any ifconfig down/up/MTU/cap-change (or detach/suspend/watchdog) on an nfe(4) interface racing the 1 Hz tick wedges two kernel threads permanently and starves one CPU's callout subsystem (system-wide timer starvation for threads on that CPU). No privilege boundary crossed.
Fix
lwkt_serialize_try + retry pattern (identical to the accepted DF-3715 /
DF-4046 family fix): never block in the tick handler.
Fix validation
Not testable in-driver on this guest (no nfe hardware); the fix.diff applies the same try-enter shape proven to unload cleanly in the DF-3715/DF-4046 negative controls.
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_testableIn-driver fix validation impossible on this guest (no nForce hardware; QEMU does not emulate forcedeth). fix.diff applies the lwkt_serialize_try + retry shape whose negative control unloaded cleanly in the DF-3715/DF-4046 family verifications.
findings/poc/DF-4051/fix.diff; family negative-control precedent in findings/poc/DF-4046/VERDICT.md
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log (ps: stopper pid 974 D2 costp vs softclock 3 slize; early-run softclock-4 starvation variant), serial_console.txt (console ends at 'IF THIS IS THE LAST nfereplica LINE, AB-BA DEADLOCK IS PROVEN' after 'nfe_tick parked ... (fired=201 ran=200) - entering callout_stop()'), build.log (-Werror clean .ko), VERDICT.md (full path:line trace)
PoC changes
v1 used a tsleep-based park-wait in the replica's stop sysctl; its timeout is delivered by the same softclock the tick parks, so the stopper starved in its own aid (D4/nfetkwt, still a permanent wedge) - v2 switched the park-wait to a busy DELAY to isolate the true block point at callout_stop/'costp'. The if_nfe.c protocol itself is modeled verbatim.
Verified recommended fix
nfe_tick: lwkt_serialize_try + 1-tick retry instead of blocking lwkt_serialize_enter (same shape as the DF-3715/DF-4046 family fix); see fix.diff
Verdict
Mechanism PROVEN on the guest kernel by nfereplica.ko, a deterministic replica of if_nfe.c's exact callout/serializer protocol (non-MP callout_init if_nfe.c:636; nfe_tick blocking lwkt_serialize_enter :2187 with unconditional under-lock self re-arm :2190; nfe_stop's synchronous callout_stop under the serializer :1593): the nfe_stop-shaped stop wedged permanently with the stopper in D2/'costp' (kern_timeout.c:920 sync wait of callout_stop) against the tick's softclock thread in 'slize' (lwkt_serialize_enter) - console ends at the AB-BA marker, two samples confirm permanence, observing shell froze, guest could not shut down cleanly. nfe(4) runtime itself is hardware-gated (guest has no nForce MCP NIC; QEMU does not emulate forcedeth), so per DF-4025/DF-4046 family convention the persisted status is inconclusive. Local DoS: any ifconfig down/up/MTU/capchange/detach racing the 1 Hz tick permanently wedges two kernel threads and one CPU's callout subsystem.
No comments yet.