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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3760 Β· 12 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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)

  1. 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.
  2. sys/dev/netif/alc/if_alc.c:3414 β€” alc_tick() does a blocking lwkt_serialize_enter(ifp->if_serializer); :3428 β€” self re-arm callout_reset(&sc->alc_tick_ch, hz, alc_tick, sc) inside the serialized section.
  3. sys/dev/netif/alc/if_alc.c:3973 β€” alc_stop() calls callout_stop(&sc->alc_tick_ch) while holding the same serializer (ASSERT_SERIALIZED(ifp->if_serializer) at :3965).
  4. 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 caller ssleep(c, &c->spin, 0, "costp", 0) (sys/kern/kern_timeout.c:923) until the callback completes.
  5. β‡’ A tick callback dequeued and parked in lwkt_serialize_enter (if_alc.c:3414) on the serializer held by the alc_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_stop returned immediately and pending=0 β€” the DF-3760 fix.diff mechanism is validated live on this kernel.
  • stop_blk (shipped shape, backgrounded): dmesg stops exactly at alcreplica[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-session ps: 1334 D5 costp sysctl kern.alcreplica.stop_blk=1 β€” D-state, wchan costp == sys/kern/kern_timeout.c:923, i.e. the alc_stop()-shaped caller permanently stuck inside callout_stop() holding the serializer while the alc_tick()-shaped callback is parked in lwkt_serialize_enter. Guest wedged (a follow-up ssh session on the wedged instance hung; a fresh one worked for ps), then vm.sh reset with-src β†’ clean boot (env.txt captured on the fresh instance: no Atheros PCI device, ifconfig -l = vtnet0 lo0, NO_ALC_KLD, no hw.alc sysctl; /boot/kernel/if_alc.ko exists only as a module file) β†’ the alc-specific instance is hardware-gated.
  • Guest /usr/src copy of if_alc.c byte-identical to the audited tree (md5 fb1a46ef2b2559de4aa5d508e03efcd5).

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_testable
baseline no→ patch + rebuild →patched clean

fix.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')
↓ fix.diffper-fix-DF-3760

Confirmed kernel references

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.