pcn_stop()/pcn_tick() AB-BA callout deadlock: synchronous callout_stop() under the ifnet serializer vs the 1 Hz self-re-arming non-MPSAFE tick parked in blocking lwkt_serialize_enter β DF-3715 family, pcn(4) instance (mechanism PROVEN on guest via protocol replica)
Summary
pcn_stat_timer is a PLAIN non-MPSAFE callout (callback executes under the Giant MP lock). pcn_tick does a BLOCKING serialize_enter and unconditionally self re-arms; pcn_stop tears it down with the synchronous callout_stop while EVERY caller holds that serializer (detach/shutdown/init-from-ioctl-watchdog-intr). A tick parked on the serializer deadlocks against the stopper: two threads sleep forever, one CPU's softclock blacks out WITH the Giant MP lock held, and the system can hang so hard it cannot shut down. Mechanism PROVEN on the guest via protocol replica (stopper D0/costp + softclock B2/slize; clean shutdown failed); fix-shape validated live (308 try-retries, zero parks, stop returns pending=0). try-enter + retry fix. 15th family member.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4365 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.6 KB | β raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| pcn_callout_replica.c | β | 5.4 KB | view raw | |
| pcn_callout_replica_fixed.c | β | 3.7 KB | view raw | |
| fix.diff | β | 666 B | view raw | |
| build.sh | β | 166 B | view raw | |
| run.sh | β | 296 B | view raw | |
| build.log | β | 11.1 KB | view raw | |
| run.log | β | 997 B | view raw | |
| fix_validation.log | β | 972 B | view raw | |
| boot.log.wedge | β | 13.2 KB | β download | |
| env.txt | β | 150 B | view raw | |
| verdict.json | β | 5.1 KB | view raw | |
| manifest.json | β | 1.5 KB | view raw |
DF-4365 β pcn_stop()/pcn_tick() AB-BA callout deadlock (DF-3715 family, pcn(4) instance)
File: sys/dev/netif/pcn/if_pcn.c Β· Severity: Medium Β· Class: CWE-667 improper locking / kernel wedge (DoS)
Bug (source)
pcn_stat_timer is a plain (non-MPSAFE) callout (callout_init at
if_pcn.c:577) whose callback pcn_tick() does a blocking
lwkt_serialize_enter(ifp->if_serializer) (if_pcn.c:879), self-re-arms at
1 Hz (callout_reset(..., hz, pcn_tick, sc) at if_pcn.c:896), and is torn
down by callout_stop(&sc->pcn_stat_timer) in pcn_stop()
(if_pcn.c:1329) β while every caller of pcn_stop() holds that same
serializer: pcn_detach (:641-643), pcn_shutdown (:1374-1376),
pcn_init (:1106, reached from pcn_ioctl SIOCSIFFLAGS :1273/:1276,
pcn_watchdog :1306-1310, pcn_intr ERR :929), and pcn_intr directly
(:912-914, interrupt context).
DragonFly's callout_stop() is synchronous: it ssleep()'s on
'costp' (kern_timeout.c:920) until an INPROG callback finishes
(kern_timeout.c:567). A tick dequeued and parked in
lwkt_serialize_enter() (lwkt_serialize.c:105) on the serializer held by
the pcn_stop() caller therefore deadlocks against it (AB-BA): the stopper
sleeps forever, the tick sleeps forever, that CPU's softclock stops
delivering ALL callouts, and β because the callout is non-MPSAFE β the
parked softclock thread holds the Giant MP lock while it sleeps.
Trigger on real hardware: ifconfig pcnX down (or up/down cycling,
MTU/flags change, watchdog reinit, detach, suspend/resume, system shutdown)
racing the 1 Hz tick.
Reproduce (guest, protocol replica β the guest has no PCnet NIC)
./build.sh # ships + builds pcn_callout_replica{,_fixed}.ko on the guest
./run.sh # loads replica, arms tick, fires pcn_stop shape in background
Expected (buggy protocol): serial console prints
pcnreplica[stop]: tick INPROG and PARKED on the serializer (fired=2 ran=1) pcnreplica[stop]: IF THIS IS THE LAST pcnreplica LINE callout_stop NEVER RETURNED (AB-BA deadlock)
and no further pcnreplica lines. Fresh ssh session shows the stopper in
D0/wchan costp and one (softclock N) thread in B/wchan slize.
The guest then fails to shut down cleanly.
Fix-shape validation (pcn_callout_replica_fixed.c, fresh guest): 308
try-enter retries during a 3 s serializer hold, callout_stop RETURNED,
pending=0, guest healthy.
Afterwards: vm.sh reset with-src (the wedge survives clean shutdown).
Fix
fix.diff β make pcn_tick() non-blocking (lwkt_serialize_try() + 1-tick
retry), the same shape validated for dc(4) in DF-3850.
DF-4365 β VERDICT
Status: inconclusive (driver-level runtime hardware-gated; mechanism PROVEN on the guest via deterministic protocol replica β DF-3715 family convention). Confidence: certain. Severity: Medium (permanent kernel wedge / local DoS on PCnet hardware).
What was proven, and how
The guest QEMU machine has a virtio-net NIC and no PCnet/PCI device, so the
pcn(4) driver cannot be exercised at device level (pcn probe requires
PCI vendor 0x1022 device 0x2000/0x2001 + Am79C97x CHIPID β if_pcn.c:101-107,
:455-466). The bug, however, is not in the device logic: it is in the
callout/serializer protocol pcn_stop()/pcn_tick() use, which is built
entirely from generic DragonFly kernel primitives. Following the DF-3715
family methodology (DF-3715/3717/3738/3760/3776/3806/3850/3903/3906/3935/
3991/4025/4046 β every one proven the same way), an out-of-tree module
replicated the exact protocol:
callout_init(&tick_c)β plain, likeif_pcn.c:577(NON-MPSAFE: callback executes under the Giant MP lock,kern_timeout.c:583);- callback = blocking
lwkt_serialize_enter(&ser)(park point, likeif_pcn.c:879vialwkt_serialize.c:105), then self re-arm (callout_reset(..., hz, ...), likeif_pcn.c:896), then exit; - stopper = a sysctl handler holding
&serwhile callingcallout_stop(&tick_c)β thepcn_stop()shape (if_pcn.c:1329), in the serialized context every real caller provides (pcn_detach:641-643,pcn_shutdown:1374-1376,pcn_init:1106βpcn_ioctl:1273/1276,pcn_watchdog:1306-1310,pcn_intr:912-914/929).
Baseline run (buggy protocol) β run.log, boot.log.wedge
pcnreplica[stop]: tick INPROG and PARKED on the serializer (fired=2 ran=1) pcnreplica[stop]: IF THIS IS THE LAST pcnreplica LINE callout_stop NEVER RETURNED (AB-BA deadlock)
Fresh ssh session (guest still pingable):
1131 D0 costp sysctl kern.pcnreplica_stop=1 <- stopper, kern_timeout.c:920
-1 B2 slize (softclock 2) <- parked tick, lwkt_serialize.c:105
D0/costp is precisely the synchronous callout_stop() wait; B2/slize is
precisely the tick blocked on the serializer the stopper holds. No further
pcnreplica console lines ever appeared (callout_stop never returned), CPU
2's softclock stopped delivering callouts, and the guest could not be shut
down cleanly afterwards (vm.sh down had to SIGKILL qemu) β matching the
family's "wedge survives shutdown" signature.
Fix-shape run (fixed protocol) β fix_validation.log
fix.diff gives pcn_tick() the lwkt_serialize_try() + 1-tick retry shape
(validated for dc(4) in DF-3850). On a fresh guest, the fixed-shape replica
was armed and the stopper held the serializer for 3 s from a background
session (cross-CPU, like the baseline):
pcnrepfix[stop]: fires=309 ran=1 retries=308 - calling callout_stop pcnrepfix[stop]: callout_stop RETURNED, pending=0 pcnrepfix[stat]: fires=310 ran=1 retries=309 pending=0
During the entire hold the tick fired 308 times, never parked (try-enter
retries only), and callout_stop() returned with pending == 0. Guest
healthy afterwards (uptime responsive). Deadlock eliminated at protocol
level; driver-level validation is impossible on this guest (no PCnet NIC).
Incidental corroboration of the detach-race mode
An earlier iteration of the fixed replica allowed MOD_UNLOAD without
stopping the armed callout; unloading with the tick armed produced
Fatal trap 12 ... Stopped at softclock_handler+0xd9 β the armed callout
executing unmapped module text. This is exactly what happens to pcn_tick
if pcn_detach()'s teardown ever misses it, corroborating the
post-detach UAF mode documented for the family (DF-3669/DF-3681) and showing
the stop-side discipline is not optional.
Why the source says this is real for pcn(4) specifically
if_pcn.c:577βcallout_init(&sc->pcn_stat_timer);(no_mpβ non-MPSAFE).if_pcn.c:879βlwkt_serialize_enter(ifp->if_serializer);(blocking).if_pcn.c:896βcallout_reset(&sc->pcn_stat_timer, hz, pcn_tick, sc);(unconditional self re-arm, under the serializer).if_pcn.c:1329βcallout_stop(&sc->pcn_stat_timer);insidepcn_stop().- All
pcn_stop()callers holdifp->if_serializerat that point (see README). DragonFlycallout_stop()waits synchronously for an INPROG callback (kern_timeout.c:920,:567), so the interleaving tick-parked-on-serializer vs stopper-in-callout_stop is a permanent AB-BA.
Verdict
The pcn(4) instance of the proven DF-3715 AB-BA callout family. On PCnet
hardware, any ifconfig pcnX down/detach/shutdown/watchdog-reinit that races
the 1 Hz pcn_tick permanently wedges two kernel threads, blacks out one
CPU's callout wheel (with the Giant MP lock held by the parked softclock),
and can make the system unable to shut down. The fix is the family fix:
never block the softclock thread on the driver serializer
(lwkt_serialize_try() + 1-tick retry), validated at protocol level on the
guest (308 retries / 0 parks / callout_stop returned / pending=0).
Guest left clean (vm.sh reset with-src after both runs).
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
inconclusivefix.diff authored against the read-only sys/ tree (git apply --check clean): pcn_tick uses lwkt_serialize_try + 1-tick retry. Validated at protocol level on the guest with pcn_callout_replica_fixed.ko (callout_stop returns, pending==0, 308 retries / 0 parks over a 3s serializer hold). Driver-level validation impossible on this guest (no PCnet NIC).
findings/poc/DF-4365/fix.diff; findings/poc/DF-4365/fix_validation.log
Confirmed kernel references
- sys/dev/netif/pcn/if_pcn.c:577
- sys/dev/netif/pcn/if_pcn.c:879
- sys/dev/netif/pcn/if_pcn.c:896
- sys/dev/netif/pcn/if_pcn.c:1329
- sys/dev/netif/pcn/if_pcn.c:641
- sys/dev/netif/pcn/if_pcn.c:1106
- sys/dev/netif/pcn/if_pcn.c:1374
- sys/dev/netif/pcn/if_pcn.c:912
- sys/kern/kern_timeout.c:567
- sys/kern/kern_timeout.c:920
- sys/kern/lwkt_serialize.c:105
Detail
Evidence (decisive lines)
findings/poc/DF-4365/VERDICT.md; run.log (session transcript: D0 costp stopper pid 1131 + B2 slize softclock 2 + shutdown-wedge note); fix_validation.log (fixed-shape: fires=309 retries=308, callout_stop RETURNED pending=0, guest healthy); build.log (both modules built clean under -Werror); boot.log.wedge (serial console with both marker lines); fix.diff (lwkt_serialize_try + 1-tick retry for pcn_tick, git apply --check clean)
PoC changes
No seed PoC. Authored pcn_callout_replica.ko (out-of-tree kmodule replicating the pcn callout protocol verbatim, including the plain/non-MPSAFE callout_init and 1 Hz re-arm) because the guest has no PCnet hardware; the deadlock is in the generic protocol pcn uses, so the replica exercises the identical kernel paths (same methodology as DF-3715/3850 family). pcn_callout_replica_fixed.ko validates the fix shape live. Replica MOD_UNLOAD originally forgot callout_stop and crashed the guest with 'softclock_handler' executing unmapped module text - itself a live corroboration of the family's post-detach UAF mode; fixed in the shipped source.
Verified recommended fix
Make pcn_tick() non-blocking: lwkt_serialize_try() with a 1-tick callout_reset retry (fix.diff, protocol-validated live).
Verdict
pcn(4) instance of the DF-3715 AB-BA callout family. Mechanism PROVEN on the guest on the first attempt by deterministic replica of if_pcn.c's exact callout/serializer protocol; pcn-specific runtime hardware-gated (inconclusive per family convention). pcn_tick (sys/dev/netif/pcn/if_pcn.c:873-899) is the callback of a PLAIN non-MPSAFE callout (callout_init at :577 -> callback executes under the Giant MP lock, kern_timeout.c:583) and does a BLOCKING lwkt_serialize_enter(ifp->if_serializer) at :879 with unconditional self re-arm at :896; pcn_stop() tears it down with callout_stop() at :1329 while holding that same serializer (callers: pcn_detach :641-643, pcn_shutdown :1374-1376, pcn_init :1106 via pcn_ioctl SIOCSIFFLAGS :1273/:1276 + pcn_watchdog :1306-1310 + pcn_intr ERR :929, and pcn_intr direct :912-914). DragonFly callout_stop synchronously waits for an INPROG callback (kern_timeout.c:1091/920 'costp', INPROG at :567). Kernel-module replica on the guest: with the pcn-shaped tick parked, callout_stop NEVER returned - stopper observed D0/wchan 'costp' (pid 1131), softclock 2 observed B2/wchan 'slize' holding the Giant MP lock (non-MPSAFE callout), serial console shows 'fired=2 ran=1' then the terminal marker line. The wedge survived a clean shutdown attempt (guest could not power off; qemu SIGKILL + snapshot revert required). Fix-shape validation on a fresh guest with the exact fix.diff protocol (lwkt_serialize_try + 1-tick retry): callout_stop RETURNED with callout_pending()==0 after 308 try-enter retries over a 3s serializer hold (fires=309 ran=1 retries=308) and zero parks - deadlock eliminated at protocol level. On PCnet hardware the trigger is any ifconfig pcnX down/up cycle, flags/MTU change, watchdog reinit, detach, suspend/resume or system shutdown racing the 1 Hz tick: permanent two-thread kernel wedge + CPU callout blackout + unclean-shutdown hang.
No comments yet.