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)
PoC verification
Evidence pack
findings/poc/DF-4219 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
- Honest device (5 real frames): walk processes exactly 5 descriptors
and exits via the
:647guard. - Device that keeps
AE_RXD_UPDATEahead 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:647never 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.
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_testableBaseline 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
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.
No comments yet.