alc_stop()/alc_tick() AB-BA deadlock: self-re-arming tick callout blocks on the driver serializer while alc_stop calls the synchronous callout_stop() holding it β permanent kernel wedge on ifconfig down/MTU change/detach/suspend (DF-3715 family, independent instance)
Summary
alc_tick is an MP-safe self-re-arming callout whose handler does a blocking lwkt_serialize_enter(ifp->if_serializer) and re-arms at hz. alc_stop tears it down with callout_stop while holding that same serializer. callout_stop is the synchronous variant and ssleeps on 'costp' until the in-progress callback completes. A tick callback dequeued and parked on the serializer therefore deadlocks against the stopper: both threads wedge permanently, the interface serializer is never released, and the wedged softclock thread stops delivering every callout on its CPU. Callers: alc_init (via ioctl up/down, watchdog, DMA-error reinit from the interrupt thread), alc_detach, alc_suspend. DETERMINISTICALLY PROVEN on the audit guest via an out-of-tree module replicating if_alc.c's exact protocol: stop never returns, ps shows 'D5 costp' (wchan == kern_timeout.c:923), guest wedged; fix-shape control (try-enter + retry) returns cleanly pending=0. Privileged local user hits a ~0.1-1% per-stop race but can retry indefinitely -> permanent system degradation. Same class as DF-3715 (if_em.c), first filed for this file.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3760 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.6 KB | β raw | |
| VERDICT.md | β | 5.5 KB | β raw | |
| alc_callout_replica.c | β | 6.9 KB | view raw | |
| Makefile | β | 78 B | β download | |
| build.sh | β | 76 B | view raw | |
| run.sh | β | 316 B | view raw | |
| build.log | β | 1.2 KB | view raw | |
| run.log | β | 1.1 KB | view raw | |
| env.txt | β | 459 B | view raw | |
| fix.diff | β | 884 B | view raw | |
| manifest.json | β | 2.2 KB | view raw | |
| verdict.json | β | 5.8 KB | view raw |
DF-3760 β alc_stop()/alc_tick() callout AB-BA deadlock (DF-3715 family)
Severity: Medium (privileged local permanent kernel deadlock / DoS) β
DragonFly sys/dev/netif/alc/if_alc.c, pass 2 (GLM 5.3).
What
alc_tick is an MP-safe, self-re-arming callout whose handler takes the
driver serializer blocking:
static void
alc_tick(void *arg)
{
...
lwkt_serialize_enter(ifp->if_serializer); /* :3414 BLOCKING */
mii_tick(mii); alc_stats_update(sc); alc_txeof(sc); alc_watchdog(sc);
callout_reset(&sc->alc_tick_ch, hz, alc_tick, sc); /* :3428 */
lwkt_serialize_exit(ifp->if_serializer);
}
alc_stop tears it down while holding that same serializer:
static void
alc_stop(struct alc_softc *sc)
{
ASSERT_SERIALIZED(ifp->if_serializer); /* :3965 */
...
callout_stop(&sc->alc_tick_ch); /* :3973 */
On this kernel callout_stop() is the synchronous variant
(sys/kern/kern_timeout.c:1091 β _callout_cancel_or_stop(..., sync=1));
when the callback is INPROG it ssleeps on "costp"
(sys/kern/kern_timeout.c:923) until the callback completes.
β If alc_tick fired and is parked inside lwkt_serialize_enter
(if_alc.c:3414) on the serializer held by the alc_stop() caller, the
stopper sleeps in callout_stop() waiting for the callback, and the
callback sleeps waiting for the serializer: permanent AB-BA deadlock.
Both threads wedge forever, the interface serializer is never released
(every later driver path on that interface blocks: ioctls, interrupts,
tick), and the affected CPU's softclock thread stops delivering all
callouts.
Callers of alc_stop with the serializer held: alc_init
(if_alc.c:3580 β every reinit: ifconfig up/down alc_ioctl :2824/:2826,
watchdog :2774/:2779, DMA-error interrupt alc_intr :3087/:3103),
alc_detach (:1616), alc_suspend (:2437).
Trigger: ifconfig alc0 down (or MTU change / detach / suspend-resume)
while the 1 Hz tick callback is dequeued-and-parked β a ~1% per-stop race
window, repeatable by a privileged local user.
Same defect as DF-3715 (if_em.c), independent instance in if_alc.c.
DF-3715's guest replica also proved the negative: on this kern_timeout.c
callout_stop's STOP unloads a racing re-arm, so the defect here is the
sync-wait deadlock, not a post-stop re-arm UAF.
Reproduce (mechanism, on this guest β deterministic)
scp alc_callout_replica.c + Makefile to guest:/root/kmod cd /root/kmod && make # build.log kldload /root/kmod/alc_callout_replica.ko sysctl kern.alcreplica.arm=1 sysctl kern.alcreplica.stop_try=1 # FIX-shape control: returns, pending=0 (sysctl kern.alcreplica.stop_blk=1 &) # shipped-shape: NEVER returns dmesg | grep 'alcreplica\[blk\]' # "IF THIS IS THE LAST alcreplica LINE," ps -o pid,stat,wchan,command -ax | grep stop_blk # -> <pid> D5 costp sysctl kern.alcreplica.stop_blk=1 == kern_timeout.c:923
The replica reproduces if_alc.c's exact protocol
(blocking lwkt_serialize_enter + callout_reset(hz) self re-arm vs
callout_stop under the serializer). Expected: the last dmesg line is the
marker, the sysctl thread sits D-state, wchan costp forever, and the
guest must be reset. The alc-specific instance itself needs AR81xx
hardware, which the audit guest lacks (env.txt).
Fix
fix.diff β mirror the DF-3715 fix: alc_tick uses
lwkt_serialize_try(); on failure it re-arms for the next tick and
returns (never blocks on the serializer, so callout_stop under the
serializer always completes; alc_stop's STOP unloads the racing re-arm β
validated live by the replica's stop_try control: callout_stop
returned, pending=0).
DF-3760 β VERDICT
Status: mechanism PROVEN on the guest kernel by deterministic replica of if_alc.c's exact callout/serializer protocol; alc-specific runtime hardware-gated β inconclusive per family convention (DF-3715 precedent), reproduced=0, impact=none-on-this-guest. Class of impact when it fires on alc hardware: permanent kernel deadlock (DoS) triggered by a privileged local user (ifconfig down/MTU/detach/suspend).
Root cause (path:line)
sys/dev/netif/alc/if_alc.c:1353βcallout_init_mp(&sc->alc_tick_ch): the tick runs on a softclock thread and manages locking by hand.sys/dev/netif/alc/if_alc.c:3414βalc_tick()does a blockinglwkt_serialize_enter(ifp->if_serializer);:3428β self re-armcallout_reset(&sc->alc_tick_ch, hz, alc_tick, sc)inside the serialized section.sys/dev/netif/alc/if_alc.c:3973βalc_stop()callscallout_stop(&sc->alc_tick_ch)while holding the same serializer (ASSERT_SERIALIZED(ifp->if_serializer)at:3965).sys/kern/kern_timeout.c:1091βcallout_stop()is the synchronous variant on this kernel (_callout_cancel_or_stop(..., sync=1)); with the callback INPROG the callerssleep(c, &c->spin, 0, "costp", 0)(sys/kern/kern_timeout.c:923) until the callback completes.- β A tick callback dequeued and parked in
lwkt_serialize_enter(if_alc.c:3414) on the serializer held by thealc_stop()caller deadlocks against the stopper (AB-BA): both threads wedge permanently. Collateral: the interface serializer is held forever (all later driver paths on that interface block β ioctls, RX/TX interrupt, tick) and the wedged softclock thread stops delivering every callout on its CPU.
Callers of alc_stop holding the serializer:
alc_init (sys/dev/netif/alc/if_alc.c:3580; reached from alc_ioctl
SIOCSIFFLAGS up :2824/down :2826, watchdog :2774/:2779, and
alc_intr DMA-error reinit :3087/:3103 β the latter from the
interrupt thread), alc_detach (:1616, serializer entered at :1615),
alc_suspend (:2437).
Race window: the tick must be INPROG at the moment the stopper is between
its serializer acquire and callout_stop(). That window includes
alc_stop's register writes plus β₯1 ms of DELAYs (:3984, :4040,
:4103), the tick period is 1 s β β0.1β1% per stop, but the trigger
(ifconfig alc0 down, MTU change, detach, suspend/resume) is repeatable
by root, making eventual occurrence plausible on long-lived boxes. The
alc_intr path (:3103) shows the same AB-BA can be entered from the
interrupt thread when the DMA-error bit latches while a tick is parked.
This is the if_alc.c instance of the class proven for if_em.c as DF-3715; identical primitives, independent file, not previously filed for alc.
Not a re-arm UAF
DF-3715's replica (same kernel) proved the negative control: after the
callback returns, callout_stop's STOP post-processing unloads a racing
re-arm (_callout_update_spinlocked, pending=0). So there is no
post-stop re-arm UAF here β the defect is purely the sync-wait deadlock.
Re-verified for the alc/fix shapes by this pack's stop_try control.
Verification performed (guest, DragonFly 6.5-DEVELOPMENT x86_64)
alc_callout_replica.ko (out-of-tree kmodule replicating if_alc.c's exact
protocol: blocking-enter + hz self re-arm "shipped shape", and
try-enter + 1-tick retry "fix shape"):
- build:
cd /root/kmod && makeβ clean with-Werror(build.log). stop_try(fix-shape control, run FIRST): under the serializer,callout_stopreturned immediately andpending=0β the DF-3760 fix.diff mechanism is validated live on this kernel.stop_blk(shipped shape, backgrounded): dmesg stops exactly atalcreplica[blk]: IF THIS IS THE LAST alcreplica LINE, THE if_alc.c AB-BA DEADLOCK IS PROVENβcallout_stop()never returned (run.log + serial console). Second-sessionps:1334 D5 costp sysctl kern.alcreplica.stop_blk=1β D-state, wchancostp==sys/kern/kern_timeout.c:923, i.e. the alc_stop()-shaped caller permanently stuck insidecallout_stop()holding the serializer while the alc_tick()-shaped callback is parked inlwkt_serialize_enter. Guest wedged (a follow-up ssh session on the wedged instance hung; a fresh one worked forps), thenvm.sh reset with-srcβ clean boot (env.txt captured on the fresh instance: no Atheros PCI device,ifconfig -l=vtnet0 lo0,NO_ALC_KLD, nohw.alcsysctl;/boot/kernel/if_alc.koexists only as a module file) β the alc-specific instance is hardware-gated.- Guest
/usr/srccopy of if_alc.c byte-identical to the audited tree (md5fb1a46ef2b2559de4aa5d508e03efcd5).
Fix
fix.diff (git-apply clean against the read-only tree): alc_tick takes
the serializer with lwkt_serialize_try(); on failure it re-arms for the
next tick (callout_reset(&sc->alc_tick_ch, 1, alc_tick, sc)) and returns
β the handler never blocks on the serializer, so alc_stop()'s
callout_stop() always makes progress, and its STOP unloads the racing
re-arm (validated live by stop_try: callout_stop returned, pending=0).
Mirrors the DF-3715 fix for if_em.c.
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.diff (git-apply clean) switches alc_tick to lwkt_serialize_try + 1-tick retry, mirroring the DF-3715 fix for if_em.c. The mechanism of the fix is validated live on the guest: the replica's fix-shape control (identical try-enter + retry protocol) shows callout_stop under the serializer returns with pending=0, no deadlock. Full driver-level validation is not possible on a guest without Atheros hardware.
fix.diff; run.log stop_try section ('alcreplica[try]: callout_stop returned, pending=0')
Confirmed kernel references
- sys/dev/netif/alc/if_alc.c:1353
- sys/dev/netif/alc/if_alc.c:3414
- sys/dev/netif/alc/if_alc.c:3428
- sys/dev/netif/alc/if_alc.c:3965
- sys/dev/netif/alc/if_alc.c:3973
- sys/dev/netif/alc/if_alc.c:3580
- sys/dev/netif/alc/if_alc.c:2826
- sys/dev/netif/alc/if_alc.c:1615
- sys/dev/netif/alc/if_alc.c:3103
- sys/kern/kern_timeout.c:1091
- sys/kern/kern_timeout.c:923
Detail
Exploit chain
Privileged local user repeatedly flaps the interface (ifconfig alc0 down / change MTU / kldunload if_alc) until a 1 Hz tick callback is dequeued-and-parked during an alc_stop; both threads wedge permanently (unkillable D-state), NIC and all serializer-sharing paths die, CPU-wide callout delivery stops on the wedged softclock thread. No privilege-escalation primitive; impact is permanent system degradation.
Evidence (decisive lines)
run.log (replica session transcript + wedged-thread ps capture '1334 D5 costp sysctl kern.alcreplica.stop_blk=1' + serial-console tail showing the marker as the last line); build.log (module built on guest, clean with -Werror); env.txt (post-reset fresh instance: no Atheros device, module file only, md5 of if_alc.c == audited tree); VERDICT.md (path:line trace); alc_callout_replica.c; fix.diff (try-enter + 1-tick retry)
PoC changes
No seed PoC. Authored alc_callout_replica.ko (out-of-tree kmodule replicating if_alc.c's callout protocol, both the shipped blocking shape and the fix.diff try-based shape) because the guest has no Atheros hardware; the deadlock is in the generic protocol alc uses, so the replica exercises the identical kernel paths. Adapted from the DF-3715 replica with the alc-specific shapes/periods (hz re-arm, single tick callout) and sysctl namespace.
Verified recommended fix
Make alc_tick() non-blocking: lwkt_serialize_try() with a 1-tick callout_reset retry (see fix.diff; DF-3715 fix pattern).
Verdict
Mechanism PROVEN on the guest kernel by deterministic replica of if_alc.c's exact callout/serializer protocol; alc-specific runtime hardware-gated (inconclusive per family convention, DF-3715 precedent). alc_tick (sys/dev/netif/alc/if_alc.c:3408-3436) is an MP callout (callout_init_mp :1353) whose callback BLOCKS in lwkt_serialize_enter (:3414) on the driver serializer and self re-arms at hz (:3428); alc_stop tears it down with callout_stop (:3973) while holding that same serializer (ASSERT_SERIALIZED :3965). Callers holding the serializer: alc_init (:3580; reached from alc_ioctl SIOCSIFFLAGS up/down :2824/:2826, watchdog :2774/:2779, alc_intr DMA-error reinit :3087/:3103), alc_detach (:1615-1616), alc_suspend (:2437). DragonFly callout_stop synchronously waits for an in-progress callback (sys/kern/kern_timeout.c:1091 -> _callout_cancel_or_stop sync=1; ssleep 'costp' at :923), so a tick callback dequeued and parked on the serializer deadlocks against the stopper (AB-BA): both threads wedge permanently, the interface serializer is never released (all later driver paths on that interface block), and the affected CPU's softclock stops delivering all callouts. Kernel-module replica on the guest (alc_callout_replica.ko, exact if_alc.c protocol): stop under a held serializer with the alc_tick-shaped handler parked -> callout_stop NEVER returned (dmesg marker is the last line; sysctl thread 1334 D5 wchan 'costp' == kern_timeout.c:923, run.log); fix-shape control (try-enter + 1-tick retry, i.e. this pack's fix.diff) -> callout_stop returned, pending=0 (fix mechanism validated live). Negative per DF-3715: on this kern_timeout.c a post-callback STOP unloads the racing re-arm, so the defect is the deadlock, not a re-arm UAF. The alc instance itself needs AR81xx hardware (guest: NO_ATHEROS_PCI, ifconfig=vtnet0 lo0, NO_ALC_KLD, no hw.alc sysctl; if_alc.ko exists only as a module file; guest src md5 == audited tree). Trigger on real hardware: ifconfig alc0 down, MTU change on a running interface, detach/kldunload, suspend/resume, DMA-error reinit - any alc_stop while alc_tick is INPROG-parked; ~0.1-1% race per stop but repeatable by root.
No comments yet.