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

ae_rx_intr() for(;;) descriptor walk has no work bound: a device that keeps AE_RXD_UPDATE set livelocks the interrupt handler holding ifp->if_serializer (DF-3990 family)

Summary

The RX walk's only exit is a descriptor without the controller-DMA'd UPDATE bit. The driver clears the bit in device-writable DMA memory and nothing stops the device from re-marking faster than the driver drains: ae_intr then never returns. No frame budget exists anywhere in if_ae.c (ale at least defines dead ALE_PROC_* knobs). NOT remotely triggerable by packet content alone - the unbounded case is device-paced (same bar as DF-3990). Walk-model-proven on the guest (honest device exits after 5; hostile device hits the 1M-iteration cap in ONE call, ring wrapped 15,625x, frame budget consulted: 0). Per-interrupt budget fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4219 Β· 10 files
FileTypeDescriptionSize
README.md β€” 1.5 KB ↓ raw
VERDICT.md β€” 3.7 KB ↓ raw
trigger.c β€” 5.0 KB view raw
build.sh β€” 214 B view raw
run.sh β€” 126 B view raw
run.log β€” 1.3 KB view raw
env.txt β€” 511 B view raw
fix.diff β€” 734 B view raw
manifest.json β€” 2.5 KB view raw
verdict.json β€” 3.8 KB view raw

DF-4219 β€” ae_rx_intr() for(;;) walk has no work bound: device-fed AE_RXD_UPDATE livelocks the serializer

sys/dev/netif/ae/if_ae.c β€” pass 2 (GLM 5.3). DF-3990 (ale) family instance for ae(4) (alc DF-3761 / jme DF-3778 / age DF-3934 family).

Build

scp -F dfbsd-qemu/config findings/poc/DF-4219/trigger.c dfbsd:/tmp/
vm.sh run_root 'cc -O2 -Wall -Wextra -o /tmp/df4219 /tmp/trigger.c && echo BUILD_4219_OK'

Clean build (-Wall -Wextra, no warnings), RC=0.

Run (safe β€” model self-caps)

vm.sh run_root '/tmp/df4219; echo RC_4219=$?'

Expected output

  1. Honest device (5 real frames): walk processes exactly 5 descriptors and exits via the :647 guard.
  2. Device that keeps AE_RXD_UPDATE ahead of the driver's clear: the model self-caps after 1,000,000 iterations in one ae_rx_intr() call (ring wrapped 15,625 times), guard :647 never went false, frame budget consulted by the driver: 0 (none exists β€” unlike ale(4), ae(4) does not even define dead ALE_PROC_*-style knobs).

Success criterion: RC_4219=0 with the [2] self-cap block printed.

Why only a model

The walk's exit depends on controller-written descriptor flags; the audit guest has no Attansic L2 NIC (no 1969:xxxx PCI device, if_ae not in X86_64_GENERIC) β€” hardware-gated per family convention (DF-3934/DF-3990/DF-3991). The unboundedness is a structural property of the loop (only exit = a non-UPDATE descriptor; no budget anywhere in the driver), demonstrated by the model and by inspection.

VERDICT.md
↓ download raw

DF-4219 β€” ae_rx_intr() has no work bound: the descriptor walk's only exit is a controller-written AE_RXD_UPDATE bit being clear, so a device that keeps marking descriptors spins the interrupt handler forever holding ifp->if_serializer (livelock DoS). DF-3990 (ale) family instance (alc DF-3761 / jme DF-3778 / age DF-3934 family), ae(4).

REPRODUCED? Not end-to-end (hardware-gated: no Attansic L2 NIC on the audit guest β€” NO_ATTANSIC_PCI_1969, if_ae not in X86_64_GENERIC, md5 of guest /usr/src copy == audited tree). Walk model built and run on the guest (run.log, RC=0). Family convention: inconclusive runtime, certain source.

ROOT CAUSE (path:line): sys/dev/netif/ae/if_ae.c:643 for (;;) { :644 rxd = (struct ae_rxd )(sc->rxd_base + sc->rxd_cur); :646 flags = le16toh(rxd->flags); <-- device-DMA'd :647 if ((flags & AE_RXD_UPDATE) == 0) break; <-- the ONLY exit :649 rxd->flags = htole16(flags & ~AE_RXD_UPDATE); <-- driver clears :657 sc->rxd_cur = (sc->rxd_cur + 1) % AE_RXD_COUNT_DEFAULT; :658 (non-SUCCESS frames: ierrors++, continue) :663 error = ae_rxeof(sc, rxd); <-- m_devget + if_input :616/:625 } :671 AE_WRITE_2(sc, AE_MB_RXD_IDX_REG, sc->rxd_cur); <-- only AFTER exit sys/dev/netif/ae/if_aereg.h:384 AE_RXD_UPDATE 0x8000. AE_RXD_COUNT_DEFAULT is 64 (if_aereg.h:106) β€” the implicit "budget" is the ring, but nothing stops the device from re-marking descriptors the driver just cleared (they are device-writable DMA memory). There is no frame budget anywhere in if_ae.c (grep: no ALE_PROC_-style knobs even exist to be dead). ae_intr runs under ifp->if_serializer (ASSERT_SERIALIZED :754), so while the walk loops: serializer held, every iteration allocates an mbuf and runs the whole input path, and the RX mailbox write never happens.

TRIGGER / THREAT: A faulty/malicious controller (errant DMA, bad silicon, hostile PCIe passthrough NIC β€” the same device trust model as DF-1452) that keeps AE_RXD_UPDATE set faster than the driver clears it. NOT remotely triggerable by packet content alone: a remote flood can only mark at most the descriptors backing real received frames, and the walk exits on the first unmarked one β€” the unbounded case needs the device itself to misbehave (device-paced livelock, same bar as DF-3990). Impact: one CPU consumed, interface + serializer wedged until reboot; mbuf pressure from the per-iteration m_devget. DoS only, no memory corruption, no privilege path.

EVIDENCE: trigger.c walk model (honest-device vs keeps-marking device) run.log guest run, RC_4219=0: honest case 5 iterations; hostile case 1,000,000 capped iterations in one call, ring wrapped 15,625x, budget consulted 0 env.txt no 1969:xxxx device; guest src md5 == audited tree fix.diff per-interrupt frame budget (git-apply clean)

POC CHANGES vs seed: none β€” no seed PoC; authored from scratch. First guest compile had a -Wunused-but-set-variable warning in the model itself (a local counter); removed and rebuilt clean before the decisive run captured in run.log.

VERDICT: source-certain structural livelock, device-paced (not remotely triggerable), hardware-gated runtime on this guest. Status: inconclusive (family convention), confidence certain. Guest left clean.

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

Baseline walk-unboundedness proven by the model; the ae-specific fix path cannot be runtime-validated (no Attansic L2 hardware on the guest). fix.diff adds the same per-interrupt budget shape validated for the DF-3990 ale family (leave the rest for the next interrupt; IMR stays enabled); git-apply clean against the audited tree.

fix.diff (git-apply --check clean); run.log [1] vs [2] contrast
↓ fix.diffper-fix-DF-4219

Confirmed kernel references

Detail

Exploit chain

On real hardware: faulty or malicious controller (bad silicon, errant DMA, hostile PCIe passthrough NIC β€” DF-1452's device model) keeps AE_RXD_UPDATE ahead of the driver's clear -> ae_intr never returns from ae_rx_intr, serializer never released, one CPU consumed and mbuf pressure from per-iteration m_devget -> ae0 + anything needing ifp->if_serializer wedged until reboot. No privilege path.

Evidence (decisive lines)

run.log (honest vs keeps-marking walk, RC_4219=0, budget consulted 0), trigger.c (walk model), env.txt (NO_ATTANSIC_PCI_1969 / no if_ae in kernel config / guest-src md5 match), VERDICT.md (path:line trace), fix.diff (per-interrupt budget, git-apply clean)

PoC changes

No seed PoC. Authored trigger.c walk model; first guest compile warned on a model-local unused-but-set variable, removed and rebuilt clean before the decisive run (run.log is from the clean build).

Verified recommended fix

Add a per-interrupt frame budget (AE_RXD_COUNT_DEFAULT) to ae_rx_intr's for(;;) walk so a device that keeps AE_RXD_UPDATE set cannot spin the handler forever (see fix.diff).

Verdict

Source-certain, runtime hardware-gated (inconclusive per family convention). (1) ae_rx_intr() (if_ae.c:643-668) has NO work bound: the for(;;) walk terminates only on a descriptor without the controller-DMA'd AE_RXD_UPDATE bit (:647); the driver clears the bit (:649) and advances rxd_cur % 64 (:657) but nothing stops the device from re-marking, and no budget exists anywhere in if_ae.c. A faulty/malicious controller keeping the bit set spins ae_intr() forever holding ifp->if_serializer (:754): each iteration m_devget+if_input (:616/:625), the AE_MB_RXD_IDX_REG mailbox write (:671) never happens β€” CPU + softint livelock until reboot (DoS). NOT remotely triggerable by packets alone (device-paced, same bar as DF-3990 ale). Walk model built and run on the guest (run.log RC=0): honest device 5 iterations/normal exit; keeps-marking device 1,000,000 capped iterations in ONE call, ring wrapped 15,625x, budget consulted 0. Guest has no 1969:2048 device, if_ae not in X86_64_GENERIC, guest src md5 == audited tree. DoS only.