ae_tick() (non-MP, self-re-arming, blocking lwkt_serialize_enter) vs ae_stop() (sync callout_stop under the serializer): permanent AB-BA deadlock (DF-3715 family)
Summary
ae arms one plain non-MPSAFE callout whose handler takes the serializer by hand and self re-arms. Every teardown stops it while HOLDING that same serializer. callout_stop is synchronous: the stopper sleeps holding the serializer while the INPROG callback parks in lwkt_serialize_enter waiting for it (holding the mplock, being non-MP) - neither side progresses. MECHANISM PROVEN LIVE on the guest by a deterministic replica of if_ae.c's exact protocol (stopper D4/costp, STILL_WEDGED_AFTER_30S, follow-on ssh timed out because the parked callback holds the mplock). ae-specific end-to-end hardware-gated. Try-enter + retry family fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4218 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.9 KB | β raw | |
| VERDICT.md | β | 4.1 KB | β raw | |
| ae_replica.c | β | 4.8 KB | view raw | |
| Makefile | β | 60 B | β download | |
| build.sh | β | 352 B | view raw | |
| run.sh | β | 432 B | view raw | |
| build.log | β | 5.8 KB | view raw | |
| run.log | β | 2.1 KB | view raw | |
| env.txt | β | 620 B | view raw | |
| fix.diff | β | 880 B | view raw | |
| manifest.json | β | 3.2 KB | view raw | |
| verdict.json | β | 4.8 KB | view raw |
DF-4218 β ae(4) tick callout vs ae_stop(): synchronous callout_stop under the serializer = AB-BA wedge (DF-3715 family)
sys/dev/netif/ae/if_ae.c β pass 2 (GLM 5.3). DF-3715 (em, PROVEN) /
DF-3760 (alc) / DF-3776 (jme) / DF-3935 (age, PROVEN) / DF-3991 (ale,
PROVEN) family instance for ae(4).
Build
scp -F dfbsd-qemu/config findings/poc/DF-4218/ae_replica.c dfbsd:/tmp/ vm.sh run_root 'mkdir -p /root/kmod/aesink && cp /tmp/ae_replica.c /root/kmod/aesink/ && \ printf "KMOD= ae_replica\nSRCS= ae_replica.c\n\n.include <bsd.kmod.mk>\n" > /root/kmod/aesink/Makefile && \ cd /root/kmod/aesink && make'
Builds clean with -Werror (full log in build.log).
Run (WEDGES THE GUEST BY DESIGN β reset with vm.sh reset with-src afterwards)
vm.sh run_root 'kldload /root/kmod/aesink/ae_replica.ko && sysctl kern.aereplica.arm=1' timeout 90 vm.sh run_root '(sysctl kern.aereplica.stop=1 &) ; sleep 8; dmesg | tail -4; ps -ax -o pid,stat,wchan,comm | grep costp' sleep 30 vm.sh run_root 'ps -ax -o pid,stat,wchan,comm | grep costp; echo STILL_WEDGED_AFTER_30S' vm.sh reset with-src
Expected output
ae_replica: tick handler BLOCKED on our serializer (fired=148 ran=147) - entering callout_stop() ae_replica: IF THIS IS THE LAST ae_replica LINE, AB-BA DEADLOCK PROVEN (DF-3715 family / ae instance) 1135 D4 costp sysctl STILL_WEDGED_AFTER_30S
No callout_stop returned line ever appears: the stopper (the sysctl
thread, playing ae_stop's role while holding the serializer) sleeps in
costp (kern_timeout.c:920 ssleep inside the synchronous callout_stop,
kern_timeout.c:1091) and the ae_tick replica β a NON-MP callout callback
parked in lwkt_serialize_enter (if_ae.c:995 shape) holding the mplock β
can never acquire the serializer. AB-BA, permanent.
Observed live on the audit guest 2026-09-18; serial console shows the BLOCKED line as the last ae_replica output; guest was reset with-src afterwards.
DF-4218 β ae(4): the 1 Hz ae_tick() callout (non-MP, self-re-arming, blocking lwkt_serialize_enter) deadlocks against every ae_stop() path, which calls the synchronous callout_stop() while holding that same serializer. DF-3715 family instance (em/alc/jme/age/ale), proven live on the audit guest by a deterministic replica of if_ae.c's exact protocol.
MECHANISM PROVEN ON THE GUEST (run.log): 'ae_replica: tick handler BLOCKED on our serializer (fired=148 ran=147) - entering callout_stop()' ps: pid 1135 stat D4 wchan costp (the stopper thread, still wedged 30+ s later; STILL_WEDGED_AFTER_30S). No "callout_stop returned" line ever printed; the serial console's last ae_replica line is the BLOCKED message. The parked NON-MP callback also holds the mplock, degrading the whole system (follow-on ssh commands timed out) β same partial-degradation signature as the PROVEN DF-3991 ale run on this same guest kernel. Guest was reset with-src afterwards (verified clean).
WHY IT IS ae(4) AND NOT JUST THE REPLICA (path:line): * callout_init(&sc->ae_tick_ch) if_ae.c:1262 β plain (non-MPSAFE) callout: _callout_init sets only CALLOUT_DID_INIT (kern_timeout.c:760-764), so the callback runs holding the mplock (kern_timeout.c:580-585). * ae_tick() if_ae.c:988-999 lwkt_serialize_enter(ifp->if_serializer); :995 (BLOCKING: atomic_intr_cond_enter -> lwkt_serialize_sleep, sys/kern/lwkt_serialize.c:105-113) mii_tick(mii); callout_reset(&sc->ae_tick_ch, hz, ae_tick, sc); :997 (self re-arm; also armed by ae_init :965) lwkt_serialize_exit(ifp->if_serializer); :998 * ae_stop() if_ae.c:219-262 ASSERT_SERIALIZED(ifp->if_serializer); :225 callout_stop(&sc->ae_tick_ch); :232 callout_stop is SYNCHRONOUS: kern_timeout.c:1091-1094 -> _callout_cancel_or_stop(..., sync=1) -> ssleep(c, ..., "costp", 0) at kern_timeout.c:920 β the stopper sleeps holding the serializer. * Every ae_stop caller holds the serializer: ae_ioctl SIOCSIFFLAGS "down" :1220-1221; ae_init :799 β itself reached from ae_intr's DMAR/DMAW-timeout / PHY-linkdown path :770-775 IN INTERRUPT CONTEXT; ae_watchdog :983; ae_detach :1417; ae_suspend :1786; ae_shutdown :1801.
AB-BA: stopper holds serializer -> sleeps in costp waiting for the INPROG callback; callback is INPROG -> parked in lwkt_serialize_enter waiting for the serializer the stopper holds (and holds the mplock while parked). Neither side progresses; the interface serializer is never released; the affected CPU's softclock stops delivering callouts.
IMPACT: permanent local DoS (interface + calling thread wedge, mplock held => system-wide degradation). Trigger: race the 1 Hz tick against any stop path (ifconfig down/up repeated, suspend, detach, or the intr-error reinit). No memory corruption, no privilege path.
WHY NOT end-to-end on the ae driver itself: no Attansic L2 NIC on the audit guest (env.txt: NO_ATTANSIC_PCI_1969; if_ae not in X86_64_GENERIC; guest /usr/src md5 429ba8f801876d3e7a39eca15612f3ed == audited tree). The replica reproduces if_ae.c's protocol character-for-character (same callout flavor, same blocking enter, same self re-arm, same stop-under-serializer) on the same kernel's real callout/serializer machinery β the same evidence bar the family set in DF-3991.
FIX: family fix β try-enter + immediate re-arm in ae_tick so the callback can always complete (fix.diff; validated shape from DF-3760/DF-3935/DF-3991).
VERDICT: mechanism PROVEN live; ae-specific end-to-end hardware-gated. Status: inconclusive (family convention), confidence certain. Guest reset with-src after the proof (verified).
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_testableBaseline mechanism proven by the replica; the ae-specific fix path cannot be runtime-validated (no Attansic L2 hardware on the guest). fix.diff is the family fix (lwkt_serialize_try + re-arm, exactly the validated DF-3760/DF-3935/DF-3991 shape) and breaks the deadlock precondition (the callback can never park on the serializer the stopper holds); git-apply clean against the audited tree.
fix.diff (git-apply --check clean); replica mechanism proof in run.log
Confirmed kernel references
- sys/dev/netif/ae/if_ae.c:1262
- sys/dev/netif/ae/if_ae.c:995
- sys/dev/netif/ae/if_ae.c:997
- sys/dev/netif/ae/if_ae.c:225
- sys/dev/netif/ae/if_ae.c:232
- sys/dev/netif/ae/if_ae.c:799
- sys/dev/netif/ae/if_ae.c:770
- sys/dev/netif/ae/if_ae.c:1221
- sys/dev/netif/ae/if_ae.c:1417
- sys/dev/netif/ae/if_ae.c:1786
- sys/kern/kern_timeout.c:1091
- sys/kern/kern_timeout.c:920
- sys/kern/lwkt_serialize.c:105
Detail
Exploit chain
On real hardware: race the 1 Hz ae_tick against any stop path (repeated ifconfig ae0 down, suspend, detach, or the intr-error reinit) while the callback is INPROG-parked on the serializer -> stopper sleeps in callout_stop('costp'), callback waits on the serializer -> permanent AB-BA wedge + mplock held => system-wide degradation until reboot. No privilege path.
Evidence (decisive lines)
run.log (replica: BLOCKED message + ps D4/costp + STILL_WEDGED_AFTER_30S + serial-console tail), build.log (kmod built on guest with -Werror, clean), env.txt (NO_ATTANSIC_PCI + no if_ae in kernel config + guest-src md5 match), ae_replica.c (annotated protocol replica), VERDICT.md, fix.diff (try-enter family fix, git-apply clean)
PoC changes
No seed PoC. Authored ae_replica.c by adapting the PROVEN DF-3991 ale replica to ae's exact protocol (non-MP callout_init :1262, hz self re-arm :997, same stop sites); executed on the guest to mechanism-proof.
Verified recommended fix
Use lwkt_serialize_try() + early re-arm in ae_tick so the callback can always complete (see fix.diff β identical to the validated DF-3760/DF-3935/DF-3991 family fix).
Verdict
MECHANISM PROVEN live on the audit guest by deterministic replica of if_ae.c's exact callout/serializer protocol (DF-3715 family, ae instance; adapted from the PROVEN DF-3991 ale replica). ae(4) arms ONE non-MP callout (callout_init if_ae.c:1262; _callout_init sets only CALLOUT_DID_INIT, kern_timeout.c:760) whose handler takes the driver serializer by hand and self re-arms (ae_tick :988-999: BLOCKING lwkt_serialize_enter :995, callout_reset(hz) :997); every teardown stops it while HOLDING that serializer (ae_stop :225 ASSERT_SERIALIZED + :232 callout_stop; callers: ae_ioctl down :1220-1221, ae_init :799 β also from the ae_intr error path :770-775 IN INTERRUPT CONTEXT β, watchdog :983, detach :1417, suspend :1786, shutdown :1801). callout_stop is synchronous (kern_timeout.c:1091 -> sync=1 -> ssleep 'costp' :920): stopper holds serializer and sleeps; INPROG callback parks in lwkt_serialize_enter (lwkt_serialize.c:105-113) waiting for that serializer (holding the mplock, being non-MP) β permanent AB-BA. Guest replica run: 'tick handler BLOCKED on our serializer (fired=148 ran=147) - entering callout_stop()', NO 'callout_stop returned' line, ps stopper D4/costp, STILL_WEDGED_AFTER_30S, follow-on ssh commands timed out (mplock held), serial console ends at the BLOCKED line; guest reset with-src and verified clean afterwards. ae-specific end-to-end trigger hardware-gated (no 1969:2048, if_ae not in X86_64_GENERIC, guest src md5 == audited tree) -> inconclusive per family convention.
No comments yet.