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

RX walk has no work bound (ALE_PROC_* knobs are dead code) β€” serializer-held livelock on line-rate flood; ale_txeof takes the 16-bit TPD_CONS_IDX unmasked vs the 256-ring β€” TX over-reclaim of still-owned mbufs (DF-3761/3778/3934 family)

Summary

(1) ale_rxeof terminates only on the controller-DMA'd CMB producer; the ALE_PROC_MIN/MAX/DEFAULT budget knobs are referenced nowhere. Each iteration does m_devget+if_input while ale_intr holds ifp->if_serializer: a line-rate small-frame flood keeps prod ahead forever => interrupt handler never returns (remotely triggerable with ordinary packets, rate not malformation). (2) ale_txeof's 16-bit device register used unmasked vs the 256-entry ring: out-of-ring garbage unloads dmamaps and m_frees every queued EOP descriptor regardless of real completion. Fix: wire the budget + mask the index.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3990 Β· 9 files
FileTypeDescriptionSize
trigger.c trigger hardware-precondition model / protocol replica 3.3 KB view raw
README.md readme build/run/expected 1.1 KB ↓ raw
VERDICT.md verdict full path:line trace 3.5 KB ↓ raw
build.sh script exact build commands 65 B view raw
run.sh script exact run commands 89 B view raw
build.log log full untrimmed build output 440 B view raw
run.log log full untrimmed runtime output 1.0 KB view raw
env.txt env guest hardware/src-md5 proof 1.1 KB view raw
fix.diff fix git-apply-able unified diff 1.6 KB view raw
README.md readme build/run/expected
↓ download raw

DF-3990 β€” ale(4) unbounded device-fed index walks (DF-3761 alc / DF-3778 jme / DF-3934 age family): (1) ale_rxeof has NO work bound β€” the ALE_PROC_MIN/MAX/DEFAULT knobs (if_alevar.h:118-121) are dead code; the interrupt handler loops while (cons < prod) with prod the controller-DMA'd CMB word, re-read on every page flip β†’ line-rate small-frame flood keeps it looping forever holding ifp->if_serializer (NIC + CPU livelock, interface wedge). (2) ale_txeof takes prod = CSR_READ_2(ALE_TPD_CONS_IDX) unmasked (16-bit, ring is 256) β€” a garbage producer walks cons past every queued descriptor freeing mbufs/dmamaps the NIC still owns (over-reclaim).

BUILD cc -O2 -Wall -Wextra -o trigger trigger.c

RUN ./trigger (walk simulation: unbounded rxeof iterations + txeof over-reclaim walk)

On the audit guest: builds and runs clean (run.log). Hardware-gated: no Atheros NIC (env.txt).

EXPECTED "[1] ... iterations completed=1000000, budget used by driver=0" "[2] ... freed ALL 40 queued frames' mbufs+dmamaps ..." TRIGGER_RC=0.

VERDICT.md verdict full path:line trace
↓ download raw

DF-3990 VERDICT β€” source-level certain, runtime hardware-gated (inconclusive)

Root cause (path:line)

(1) ale_rxeof work-unbounded walk β€” if_ale.c:2153-2254

2176: prod = *rx_page->cmb_addr;          /* controller-DMA'd CMB word */
2177: for (prog = 0; ; prog++) {
2178:         if (rx_page->cons >= prod)
2179:                 break;

The only termination condition is the device-fed producer offset, refreshed from the NEW page's CMB on every page flip (ale_rx_update_page :2091). There is no per-interrupt frame budget: ALE_PROC_MIN/ALE_PROC_MAX/ ALE_PROC_DEFAULT are defined at if_alevar.h:118-121 and referenced NOWHERE (grep confirms) β€” dead knobs, exactly the DF-3934 (age) pattern. Each iteration does m_devget + ifp->if_input while holding ifp->if_serializer (ale_intr is serialized, :1969). A line-rate small- frame flood β€” precisely the workload the driver's own comment at :2224 calls its bottleneck β€” keeps prod ahead of cons indefinitely: the interrupt handler never returns, the serializer is never released, ifconfig/ioctl/watchdog on ale0 wedge, and the CPU spins in softint context. Remote-triggerable DoS/livelock (no malformed packets needed β€” just rate).

(2) ale_txeof unmasked 16-bit producer β€” if_ale.c:2026-2047

2026: prod = CSR_READ_2(sc, ALE_TPD_CONS_IDX);   /* 16-bit device reg */
2027: cons = sc->ale_cdata.ale_tx_cons;
2032: for (prog = 0; cons != prod; prog++, ALE_DESC_INC(cons, ALE_TX_RING_CNT)) {
2034:         if (sc->ale_cdata.ale_tx_cnt <= 0)
2035:                 break;
...
2042:         bus_dmamap_unload(sc->ale_cdata.ale_tx_tag, txd->tx_dmamap);
2044:         m_freem(txd->tx_m);

prod is not masked into the 256-entry ring. A device-fed value outside 0..255 makes cons != prod unconditionally true; the walk is bounded only by ale_tx_cnt reaching 0, freeing the mbuf AND unloading the dmamap of every queued EOP descriptor β€” reclaiming buffers whose descriptors the NIC may still own (premature m_freem β†’ NIC DMAs from freed/reused memory β†’ info leak to the wire, or double-unload class confusion). Same fold as DF-3934's "16-bit CMB producer indices not masked to ring sizes ... serializer-held livelock DoS and TX over-reclaim".

Verification on the guest (honest)

  • trigger.c simulates both walks; builds and runs on the guest (run.log RC=0: 10^6 iterations with no budget consumed; txeof walk frees all 40 queued frames on an out-of-ring prod).
  • Hardware-gated: no 1969:1026 device, no hw.ale sysctl; if_ale code is in the kernel but ale_intr/ale_rxeof never execute. Inconclusive per family convention.

Impact

(1) is REMOTELY triggerable on real hardware (flood) β€” interface + CPU wedge; Medium. (2) requires a faulty/malicious controller β€” over-reclaim / leak; folded into the same finding per the DF-3934 precedent.

Fix

fix.diff: (1) wire up ALE_PROC_DEFAULT as a per-interrupt budget in ale_rxeof (break; INTR_RX_PKT / IM timer re-arms β€” caveat: if the device needs a kick for backlog, a follow-up should re-arm the moderation timer explicitly); (2) mask CSR_READ_2(ALE_TPD_CONS_IDX) & (ALE_TX_RING_CNT-1). git-apply clean; not runtime-validable on this guest.

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 adds the ALE_PROC_DEFAULT budget break and the ring mask. Cannot be runtime-validated: no Atheros device on the guest. Mechanical, bounds/termination-only change on an already-compiled path; git-apply clean.

fix.diff; hardware absence proof in env.txt
↓ fix.diffper-fix-DF-3990

Confirmed kernel references

Detail

Exploit chain

On real hardware: (1) remote line-rate small-packet flood to ale0 -> ale_intr loops in ale_rxeof holding the serializer -> ale0 ifconfig/netif wedge + CPU softint livelock (DoS); (2) faulty/malicious controller writes garbage TPD_CONS_IDX -> all queued TX mbufs freed early -> NIC DMAs from reused memory (leak to wire) / double-unload confusion. No privilege path.

Evidence (decisive lines)

run.log (10^6-iteration no-budget walk + txeof over-reclaim walk, RC=0), env.txt (NO_ATHEROS_PCI / no hw.ale), VERDICT.md (path:line trace + dead-knob grep proof), trigger.c, fix.diff (budget + mask, git-apply clean)

PoC changes

No seed PoC. Authored trigger.c walk simulation; first compile failed (redefined loop variable in the model itself), fixed and re-run to RC=0; honest hardware-absent verification per family convention (DF-3934 pattern).

Verified recommended fix

Wire ALE_PROC_DEFAULT as a per-interrupt frame budget in ale_rxeof; mask TPD_CONS_IDX to (ALE_TX_RING_CNT-1) in ale_txeof (see fix.diff).

Verdict

Source-level certain, runtime hardware-gated (inconclusive per family convention). (1) ale_rxeof (if_ale.c:2153-2254) has NO work bound: the loop terminates only on the controller-DMA'd CMB producer offset (prod = *cmb_addr :2176, re-read from the new page's CMB on every page flip, ale_rx_update_page :2091); ALE_PROC_MIN/MAX/DEFAULT (if_alevar.h:118-121) are dead knobs referenced nowhere. Each iteration does m_devget + if_input while ale_intr holds ifp->if_serializer (:1969 ASSERT_SERIALIZED) - a line-rate small-frame flood (the driver's own :2224 comment calls m_devget its bottleneck) keeps prod ahead of cons forever: interrupt handler never returns, serializer never released, interface + CPU livelock. REMOTELY triggerable on real hardware with ordinary packets (rate, no malformation needed). (2) ale_txeof (:2026) takes the 16-bit TPD_CONS_IDX register unmasked against the 256-entry ring; an out-of-ring producer makes cons != prod always true and the walk - bounded only by ale_tx_cnt reaching 0 (:2034) - unloads dmamaps and m_frees the mbuf of EVERY queued EOP descriptor regardless of real completion (over-reclaim; premature m_freem of buffers the NIC still owns). Same fold as DF-3934 (age): livelock DoS + TX over-reclaim in one finding. Runtime: guest has no Atheros device (env.txt); trigger.c walk-simulation builds and runs on the guest (run.log RC=0; first model compile failed on a shadowed variable, fixed in place). Guest src md5 ae824d7a4429b82f0ad92a72b7c91da8 == audited tree.