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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3935 Β· 8 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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:2197 callout_reset(&sc->age_tick_ch, hz, age_tick, sc); if_age.c:2199 lwkt_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:2534 callout_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_testable
baseline no→ patch + rebuild →patched clean

Fix 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
↓ fix.diffper-fix-DF-3935

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.