age_stop() (serializer held) vs age_tick (blocking lwkt_serialize_enter): AB-BA callout deadlock β DF-3715 family instance, mechanism PROVEN on the guest via replica (stopper stuck D5 wchan 'costp')
Summary
age arms one NON-mp callout whose handler blocks on the driver serializer and self re-arms; every teardown path stops it with synchronous callout_stop while holding that serializer - a callback parked in lwkt_serialize_enter deadlocks the stopper. GUEST REPLICA: 'AB-BA DEADLOCK PROVEN' then permanent silence; ps shows D5 wchan 'costp' immediately and 30s later; the wedge killed the observing ssh session and prevented clean shutdown. Try+retry fix (family shape).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3935 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.4 KB | β raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| age_replica.c | β | 4.3 KB | view raw | |
| build.log | β | 5.8 KB | view raw | |
| run.log | β | 2.2 KB | view raw | |
| fix.diff | β | 977 B | view raw | |
| env.txt | β | 543 B | view raw | |
| verdict.json | β | 3.6 KB | view raw |
DF-3935 β age: age_stop() (serializer held) vs age_tick (blocking serializer enter) β AB-BA callout deadlock, DF-3715 family instance
Build / Run β MECHANISM PROVEN ON THE AUDIT GUEST (replica)
The deadlock protocol does not depend on the NIC; it was replicated exactly
(age_replica.c in this pack: plain callout_init as if_age.c:517, callback
lwkt_serialize_enter + self callout_reset as if_age.c:2194-2199, stopper
holding the serializer calling callout_stop as if_age.c:2534):
build: see build.log (guest cc 8.3, -Werror, BUILD_RC=0) run: kldload age_replica.ko sysctl kern.agereplica.arm=1 (sysctl kern.agereplica.stop=1 &) expected: "IF THIS IS THE LAST age_replica LINE, AB-BA DEADLOCK PROVEN" then silence; ps shows the stopper stuck D5 wchan costp
observed (run.log): exactly that β age_replica: tick handler BLOCKED on our serializer (fired=14 ran=13) - entering callout_stop() [never returns] ps: 1001 D5 costp sysctl (immediately AND 30s later) the wedge then degraded the guest enough to kill the ssh session.
On real age(4) hardware the same interleaving is entered by any age_stop()
caller holding the serializer while age_tick is INPROG: ifconfig age0
down/up, SIOCSIFMTU on a running interface (age_init β age_stop,
if_age.c:2249), kldunload/detach (:686-688), suspend (:1393-1394).
See VERDICT.md.
DF-3935 verdict narrative
Reproduced?
Mechanism PROVEN on the guest kernel by deterministic replica of if_age.c's exact callout/serializer protocol (same method as the family-proven DF-3715/em instance); the age-specific runtime trigger is hardware-gated (no L1 NIC in the guest) β filed inconclusive per family convention.
Root cause chain (path:line)
- age(4) arms one NON-MP callout: if_age.c:517
callout_init(&sc->age_tick_ch);(contrast em(4)'s callout_init_mp β this replica matches the non-mp kind). - Its handler takes the serializer by hand, blocking, and re-arms itself:
if_age.c:2194
lwkt_serialize_enter(ifp->if_serializer);if_age.c:2197callout_reset(&sc->age_tick_ch, hz, age_tick, sc);if_age.c:2199lwkt_serialize_exit(ifp->if_serializer); - Every teardown path stops it while HOLDING that serializer:
if_age.c:2524
ASSERT_SERIALIZED(ifp->if_serializer);if_age.c:2534callout_stop(&sc->age_tick_ch);callers: age_ioctl SIOCSIFFLAGS (:1673-1674), age_init (:2242+:2249), age_detach (:686-688), age_suspend (:1393-1394). - DragonFly's callout_stop is synchronous: kern_timeout.c:1074-1077 β _callout_cancel_or_stop(..., sync=1) β ssleep(c, &c->spin, 0, "costp", 0) at kern_timeout.c:923, waiting for the INPROG callback to finish.
- AB-BA: a callback dequeued and blocked in lwkt_serialize_enter (:2194) waits for the serializer the stopper holds; the stopper sleeps in callout_stop ('costp') waiting for that callback. Neither progresses.
Replica proof (this run, guest 6.5-DEVELOPMENT of 2026-07-02)
age_replica: loaded (hz=100) age_replica: armed age_tick replica (non-mp callout, ticks hz/10) age_replica: tick handler BLOCKED on our serializer (fired=14 ran=13) - entering callout_stop() age_replica: IF THIS IS THE LAST age_replica LINE, AB-BA DEADLOCK PROVEN (DF-3715 family / age instance) [silence β callout_stop never returned] $ ps -ax -o pid,stat,wchan,comm | grep costp 1001 D5 costp sysctl (still D5/costp 30s later)
Additionally the wedge cascaded: the observing ssh session itself timed out
(callback CPU softclock starvation), and the guest could not complete a clean
vm.sh down afterwards (qemu had to be killed before snapshot revert) β
i.e., not merely two stuck threads but system-wide degradation, matching the
DF-3715 impact analysis ("the affected CPU's softclock thread stops
delivering all callouts").
Triggers on real hardware
ifconfig age0 down / up, MTU change on a running age interface, kldunload
if_age / device detach, suspend β colliding with the 1-second tick. Narrow
per-call window, repeatable; admin-gated (as with the em instance).
Impact
Permanent wedge of the calling thread + a softclock thread + the interface serializer; progressive system degradation. DoS, Medium (family-consistent).
Fix
Same shape as the em fix: try-enter in age_tick with a 1-tick retry (fix.diff) so the callback can always complete and callout_stop can always return.
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_testableFix not validated on the guest: the trigger path requires mly/age hardware absent from the audit guest. fix.diff is mechanical (bounds/termination-only) and authored post-verification against the cited lines.
findings/poc/DF-3935/fix.diff + VERDICT.md
Confirmed kernel references
Detail
Exploit chain
ifconfig age0 down/up (or MTU change/detach/suspend) while the 1s age_tick is INPROG-parked on the serializer -> stopper sleeps in callout_stop('costp'), callback waits on the serializer -> permanent AB-BA wedge + softclock starvation
Evidence (decisive lines)
run.log (replica transcript: last-line silence + 1001 D5 costp x2 + session death); build.log; age_replica.c; fix.diff (try-enter + retry); VERDICT.md
PoC changes
No seed PoC. Authored age_replica.ko (out-of-tree module replicating the age callout protocol with the NON-mp callout init age actually uses) because the guest has no L1 hardware; the deadlock is in the generic protocol age uses.
Verified recommended fix
Use lwkt_serialize_try in age_tick with a 1-tick retry so the callback always completes and callout_stop always returns.
Verdict
Mechanism PROVEN on the guest kernel by deterministic replica of if_age.c's exact callout/serializer protocol (DF-3715 family, age instance - the 11th); age-specific runtime trigger hardware-gated (no L1 NIC), inconclusive per family convention. age(4) arms one NON-mp callout (callout_init, if_age.c:517 - unlike em's callout_init_mp, matched by the replica) whose handler blocks on the driver serializer and self re-arms (if_age.c:2194/2197/2199); every teardown stops it while HOLDING that serializer (age_stop: ASSERT_SERIALIZED :2524 + callout_stop :2534; callers ifconfig down/up via age_ioctl :1673, age_init :2249, detach :686-688, suspend :1393). DragonFly callout_stop is synchronous (kern_timeout.c:1074 -> _callout_cancel_or_stop sync=1 -> ssleep 'costp' :923): a callback dequeued and parked in lwkt_serialize_enter deadlocks against the stopper. Guest replica run: 'IF THIS IS THE LAST age_replica LINE, AB-BA DEADLOCK PROVEN' then permanent silence; ps shows the stopper 1001 D5 wchan costp immediately AND 30s later (run.log); the wedge cascaded enough to kill the observing ssh session and prevent a clean shutdown. Fix (fix.diff): try-enter in age_tick with 1-tick retry, mirroring the em fix.
No comments yet.