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

alc_rxintr() Rx walk has no work bound β€” the process_limit knob exists but is never read: a remote line-rate flood keeps the return ring perpetually non-empty so the interrupt handler loops forever holding the serializer (NIC + CPU livelock); FreeBSD bounds this loop, DFly msk(4) bounds its own

Summary

alc_rxintr walks the Rx return ring in an unbounded loop, exiting only on a non-VALID entry. The driver's hw.alcN.process_limit knob (ALC_PROC_DEFAULT 64) - precisely the bound this loop should use - is never referenced anywhere else in the driver: a dead knob. FreeBSD bounds the identical loop ('if (count-- <= 0) break;'), and DragonFly's sibling msk(4) bounds its own walk - the bound was intended and lost. alc_intr disables the NIC interrupt for the whole walk and the walk runs holding the interface serializer, while alc_newbuf's per-frame cost is slower than 1.488 Mpps line-rate arrival. Remote, unauthenticated, default-config attacker floods alc0 at line rate: the return ring never drains, the loop never terminates -> the NIC's interrupt thread pins one CPU at 100% indefinitely, every other driver entry point blocks forever on the serializer, and callout delivery starves on that CPU. Interface unusable until the flood stops. Availability only. Fix: bound the loop with the existing knob (alc_intr's post-loop interrupt re-enable makes the device immediately re-raise RX, so no packets are lost).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3761 Β· 10 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
trigger.c β€” 2.5 KB view raw
build.sh β€” 126 B view raw
run.sh β€” 227 B view raw
run.log β€” 546 B view raw
env.txt β€” 459 B view raw
fix.diff β€” 1.0 KB view raw
manifest.json β€” 2.0 KB view raw
verdict.json β€” 4.5 KB view raw

DF-3761 β€” alc_rxintr() has no work bound; process_limit knob is dead

Severity: Medium (remote-triggerable NIC/CPU livelock on default config) β€” DragonFly sys/dev/netif/alc/if_alc.c, pass 2 (GLM 5.3).

What

alc_rxintr() (sys/dev/netif/alc/if_alc.c:3222-3289) processes Rx return ring entries in an unbounded loop:

for (prog = 0; (ifp->if_flags & IFF_RUNNING) != 0;) {
    rrd = &sc->alc_rdata.alc_rr_ring[rr_cons];
    status = le32toh(rrd->status);
    if ((status & RRD_VALID) == 0)
        break;
    ...
    alc_rxeof(sc, rrd); /* fresh cluster + dmamap load per buffer */
}

The driver exposes exactly the knob meant to bound this β€” hw.alcN.process_limit (alc_sysctl_node, if_alc.c:1696-1713, ALC_PROC_DEFAULT 64, if_alcvar.h:71-73) β€” but nothing in the driver ever consults it; grep on the guest shows the only references are the sysctl setup (run.log). It is a dead knob.

FreeBSD bounds the identical loop (alc_rxintr(sc, int count): if (count-- <= 0) break;), and DragonFly's sibling driver msk(4) bounds its Rx walk the same way (if_msk.c:3225).

Impact

Under a sustained line-rate flood (1.488 Mpps of 64-byte frames at 1 GbE β€” remote, unauthenticated, default config):

  • per frame the loop performs an m_getcl cluster allocation + bus_dmamap_load_mbuf_segment + RRS processing + if_input enqueue β€” provably slower than line-rate arrival, so the return ring stays perpetually non-empty and the loop never terminates;
  • alc_intr disabled the NIC interrupt at entry (if_alc.c:3082) and only re-enables it after the loop (if_alc.c:3110) β€” there is no exit and no moderation;
  • the whole loop runs in the interrupt thread holding the interface serializer β†’ one CPU pinned at 100% forever, every other driver entry point (ifconfig, ioctls, tick) blocks indefinitely, and callouts starve on that CPU.

Fix: bound the loop with the existing knob (fix.diff, ports FreeBSD's count-- <= 0 idiom; alc_intr's re-enable at :3110 makes the device immediately re-raise the RX interrupt if work remains, so no packets are lost).

Reproduce

Trigger needs AR81xx hardware + a packet generator (documented in trigger.c). The audit guest has no Atheros device (env.txt/run.log: NO_ATHEROS_PCI, ifconfig -l = vtnet0 lo0, no hw.alc sysctl), so runtime verification is honestly inconclusive per family convention; source-level certainty is established by the guest-side greps in run.log (knob referenced only at :1697-1711 = setup; loop at :3236 has no bound) and the FreeBSD/msk comparison.

VERDICT.md
↓ download raw

DF-3761 β€” VERDICT

Status: source-level certain (loop provably unbounded, knob provably dead); runtime honestly inconclusive β€” the trigger is a remote line-rate flood against AR81xx hardware the audit guest does not have. reproduced=0, impact=none-on-this-guest; class ceiling on real hardware: remote-triggerable NIC + single-CPU livelock (DoS) on default config.

Root cause (path:line)

  1. sys/dev/netif/alc/if_alc.c:3236 β€” for (prog = 0; (ifp->if_flags & IFF_RUNNING) != 0;): the Rx return ring walk has no work bound; it exits only when an entry lacks RRD_VALID (:3239-3240) or the interface stopped running.
  2. sys/dev/netif/alc/if_alc.c:1696-1713 β€” the driver creates and clamps hw.alcN.process_limit (ALC_PROC_MIN 16 / ALC_PROC_MAX 255 / ALC_PROC_DEFAULT 64, sys/dev/netif/alc/if_alcvar.h:71-73) β€” and then never reads it anywhere (guest-side grep, run.log: references only at :1697-1711). Dead knob.
  3. sys/dev/netif/alc/if_alc.c:3082 / :3110 β€” alc_intr disables the NIC interrupt on entry (INTR_DIS_INT) and re-enables it only after alc_rxintr returns; combined with (1) an unterminating walk means the handler never re-arms interrupt delivery and never yields.
  4. Per-iteration cost: alc_rxeof (:3248) β†’ alc_newbuf (:3332, m_getcl + bus_dmamap_load_mbuf_segment per Rx buffer, :3187-3199) + if_input enqueue β€” ΞΌs-scale, i.e. slower than 1.488 Mpps line-rate arrival, so under a sustained flood the return ring is perpetually non-empty and the loop never exits.

Divergence proof: FreeBSD bounds the identical loop β€” sys/dev/alc/if_alc.c (head) alc_rxintr(struct alc_softc *sc, int count) with if (count-- <= 0) break; at the loop head; DragonFly's sibling msk(4) likewise (sys/dev/netif/msk/if_msk.c:3225, if (rxprog > sc->msk_process_limit)). The knob's presence in the DFly alc driver without a reader is strong evidence the bound was intended and lost.

Impact analysis

Remote, unauthenticated, default configuration (only needs the NIC to be up): flood the interface at line rate β†’

  • the NIC's interrupt thread spins in alc_rxintr forever, pinning one CPU at 100%;
  • it holds the interface serializer for the whole walk (alc_intr asserts the serializer, :3075), so every other driver entry point β€” ifconfig, any ioctl on the interface, alc_tick β€” blocks indefinitely;
  • kernel threads at lower priority than the (realtime-ish) interrupt thread on that CPU starve, including callout/softclock delivery.

Interface recovery requires the flood to stop; the interface is unusable for any other traffic in the meantime (RX monopolizes the ring). Not a memory-safety bug; a service-availability bug of exactly the class process_limit exists to prevent.

Verification performed (guest, DragonFly 6.5-DEVELOPMENT x86_64)

  • Presence probe (run.log, env.txt): NO_ATHEROS_PCI, ifconfig -l = vtnet0 lo0, NO_ALC_KLD, sysctl hw.alc β†’ unknown oid, /boot/kernel/if_alc.ko present only as a module file β†’ the alc instance cannot be exercised at runtime; honestly inconclusive per family convention.
  • Source-level proof on the guest itself (run.log): grep -n 'alc_process_limit' /usr/src/sys/dev/netif/alc/if_alc.c β†’ only :1697-1711 (sysctl setup); the loop at :3236 has no reference to it.
  • Guest /usr/src if_alc.c byte-identical to audited tree (md5 fb1a46ef2b2559de4aa5d508e03efcd5, env.txt).
  • FreeBSD head comparison fetched and quoted in README/finding (alc_rxintr(sc, count), if (count-- <= 0) break;).

Fix

fix.diff (git-apply clean): port FreeBSD's idiom β€” take n = sc->alc_process_limit before the loop and if (n-- <= 0) break; at the loop head. When the bound trips, alc_intr's trailing CSR_WRITE_4(sc, ALC_INTR_STATUS, 0x7FFFFFFF) (:3110) re-enables interrupts and the still-non-empty ring immediately re-raises the RX interrupt, so no packets are lost β€” the bound only restores scheduler fairness (and finally makes the admin-visible knob functional).

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) ports FreeBSD's count idiom using the existing knob; alc_intr's post-loop interrupt re-enable (:3110) makes the device immediately re-raise RX if work remains, so the bound costs no packets. Cannot be runtime-validated without AR81xx hardware.

fix.diff
↓ fix.diffper-fix-DF-3761

Confirmed kernel references

Detail

Exploit chain

Remote unauthenticated attacker on a network segment of an alc(4) interface sends a sustained line-rate small-packet flood; the interface's interrupt thread enters alc_rxintr and never exits, pinning one CPU and holding the serializer so the interface (and all its ioctls/timers) wedges until the flood stops. No memory corruption, no privilege escalation.

Evidence (decisive lines)

run.log (presence probe NO_ATHEROS_PCI / unknown oid hw.alc + guest-side grep proving the knob has no reader + msk comparison), env.txt (fresh instance state, md5 of if_alc.c == audited tree), VERDICT.md (path:line trace incl. FreeBSD head comparison), trigger.c (annotated remote-flood precondition), fix.diff (count bound, git-apply clean)

PoC changes

No seed PoC. Authored trigger.c as an annotated remote-flood precondition document (nothing meaningful to execute without AR81xx hardware) plus a guest-side source-proof run.sh; honest hardware-absent verification per family convention.

Verified recommended fix

Bound the alc_rxintr loop with the existing alc_process_limit knob: n = sc->alc_process_limit; if (n-- <= 0) break; at the loop head (see fix.diff).

Verdict

Source-level certain, runtime honestly inconclusive (hardware-gated). alc_rxintr (sys/dev/netif/alc/if_alc.c:3222-3289) walks the Rx return ring with no work bound (:3236): the loop exits only on a non-VALID entry or IFF_RUNNING clearing. The driver's process_limit knob (if_alc.c:1696-1713, ALC_PROC_DEFAULT 64, if_alcvar.h:71-73) - exactly the bound this loop should use - is never read anywhere in the driver (guest-side grep: references only at :1697-1711); FreeBSD bounds the identical loop (alc_rxintr(sc, int count), 'if (count-- <= 0) break;') and DragonFly's msk(4) bounds its own walk (if_msk.c:3225), proving the bound was intended and lost. Attack: a remote, unauthenticated, default-config attacker floods alc0 at line rate (1.488 Mpps small frames at 1GbE); the per-frame m_getcl + bus_dmamap_load_mbuf_segment + if_input cost (alc_newbuf :3187-3199 via alc_rxeof :3332) exceeds arrival rate, so the return ring stays perpetually non-empty and the loop never terminates. alc_intr has the NIC interrupt disabled for the whole walk (:3082, re-enabled only at :3110) and the walk runs holding the interface serializer (:3075): the interrupt thread pins one CPU at 100% indefinitely, every other driver entry point (ifconfig, ioctls, alc_tick) blocks forever, and callout delivery starves on that CPU - a remote-triggerable NIC + single-CPU livelock (DoS). The audit guest has no Atheros device (env.txt/run.log: NO_ATHEROS_PCI, ifconfig=vtnet0 lo0, NO_ALC_KLD, no hw.alc sysctl; if_alc.ko present only as a module file; guest src md5 == audited tree), so the instance itself cannot be fired; per family convention recorded as inconclusive. Not a memory-safety bug: availability only.