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)
PoC verification
Evidence pack
findings/poc/DF-3761 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_getclcluster allocation +bus_dmamap_load_mbuf_segment+ RRS processing +if_inputenqueue β provably slower than line-rate arrival, so the return ring stays perpetually non-empty and the loop never terminates; alc_intrdisabled 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.
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)
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 lacksRRD_VALID(:3239-3240) or the interface stopped running.sys/dev/netif/alc/if_alc.c:1696-1713β the driver creates and clampshw.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.sys/dev/netif/alc/if_alc.c:3082/:3110βalc_intrdisables the NIC interrupt on entry (INTR_DIS_INT) and re-enables it only afteralc_rxintrreturns; combined with (1) an unterminating walk means the handler never re-arms interrupt delivery and never yields.- Per-iteration cost:
alc_rxeof(:3248) βalc_newbuf(:3332,m_getcl+bus_dmamap_load_mbuf_segmentper Rx buffer,:3187-3199) +if_inputenqueue β ΞΌ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_rxintrforever, pinning one CPU at 100%; - it holds the interface serializer for the whole walk
(
alc_intrasserts 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.kopresent 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/srcif_alc.c byte-identical to audited tree (md5fb1a46ef2b2559de4aa5d508e03efcd5, 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_testablefix.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
Confirmed kernel references
- sys/dev/netif/alc/if_alc.c:3236
- sys/dev/netif/alc/if_alc.c:3239
- sys/dev/netif/alc/if_alc.c:3248
- sys/dev/netif/alc/if_alc.c:3332
- sys/dev/netif/alc/if_alc.c:3187
- sys/dev/netif/alc/if_alc.c:1696
- sys/dev/netif/alc/if_alc.c:3082
- sys/dev/netif/alc/if_alc.c:3110
- sys/dev/netif/alc/if_alc.c:3075
- sys/dev/netif/alc/if_alcvar.h:71
- sys/dev/netif/msk/if_msk.c:3225
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.
No comments yet.