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

ae_stop_rxmac()/ae_stop_txmac() decrement their idle-wait loop counter (i--): a non-idle device busy-wedges the kernel thread forever, timeout branch unreachable

Summary

Both idle-wait loops are 'for (i = 0; i < AE_IDLE_TIMEOUT; i--)'. i only decrements from 0, so the guard is permanently true: unless the device happens to report idle, the loop busy-waits in DELAY(100) forever holding ifp->if_serializer, and the post-loop timeout message can never fire. The sibling loops correctly use i++. A surprise-removed/halted/errored device (MMIO 0xFFFFFFFF -> IDLE bits stuck) or malicious passthrough NIC spins every stop path forever: ifconfig down, ae_init (also from ae_intr's error path in interrupt context), watchdog, detach, suspend, shutdown, miibus statchg. Termination-model-proven on the guest (as-written hits the 10M-iteration cap with the timeout branch never taken; i++ variant exits at exactly 100). Two-character i++ fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4217 Β· 10 files
FileTypeDescriptionSize
README.md β€” 1.7 KB ↓ raw
VERDICT.md β€” 3.4 KB ↓ raw
stoploop.c β€” 3.6 KB view raw
build.sh β€” 216 B view raw
run.sh β€” 126 B view raw
run.log β€” 1.1 KB view raw
env.txt β€” 821 B view raw
fix.diff β€” 621 B view raw
manifest.json β€” 2.6 KB view raw
verdict.json β€” 3.9 KB view raw

DF-4217 β€” ae(4) idle-wait loops count DOWN (i--): non-idle device wedges the kernel thread forever

sys/dev/netif/ae/if_ae.c β€” pass 2 (GLM 5.3).

Build

scp -F dfbsd-qemu/config findings/poc/DF-4217/stoploop.c dfbsd:/tmp/
vm.sh run_root 'cc -O2 -Wall -Wextra -o /tmp/df4217 /tmp/stoploop.c && /tmp/df4217'

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

Run

vm.sh run_root '/tmp/df4217; echo RC_4217=$?'

Expected output

The model runs the exact guard/increment/exit shape of the two loops (for (i = 0; i < AE_IDLE_TIMEOUT; i--), AE_IDLE_TIMEOUT=100) against a register stub that never reports idle (a surprise-removed / halted device whose MMIO reads return 0xFFFFFFFF):

  1. As written (i--): the guard i < 100 is permanently true (i only decrements from 0). The model self-caps at 10,000,000 iterations β€” the kernel loop has no cap β€” and shows the post-loop if (i == AE_IDLE_TIMEOUT) timeout branch is unreachable (i = -9999999 β‰  100, and it can never rise to 100 again).
  2. Fixed (i++): the same dead device terminates after exactly 100 reads (10 ms) and the "timed out while stopping Rx/Tx MAC." message fires.

Success criterion: [1] shows 10,000,000 capped iterations with the timeout branch NOT taken; [2] shows 100 reads. RC_4217=0.

Why only a model (no end-to-end run)

The loop's exit depends on the physical device reporting idle. The audit guest has no Attansic L2 NIC (no 1969:xxxx PCI device; if_ae not in X86_64_GENERIC) β€” hardware-gated per the DF-3934/DF-3990/DF-3991 family convention. The termination defect itself is a pure control-flow property, demonstrated by the model and by inspection.

Guest was left clean (this run never wedges anything).

VERDICT.md
↓ download raw

DF-4217 β€” ae_stop_rxmac()/ae_stop_txmac() idle-wait loops decrement their loop counter (i--), so a device that never reports idle spins the kernel thread in DELAY(100) forever holding ifp->if_serializer; the intended 10 ms timeout can never fire.

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). Termination-mechanism model built and run on the guest (run.log, RC=0); the control-flow defect is certain by inspection. Family convention (DF-3934/DF-3990/DF-3991): inconclusive runtime, certain source.

ROOT CAUSE (path:line): sys/dev/netif/ae/if_ae.c:288 ae_stop_rxmac(): for (i = 0; i < AE_IDLE_TIMEOUT; i--) { / <-- i--, should be i++ / val = AE_READ_4(sc, AE_IDLE_REG); if ((val & (AE_IDLE_RXMAC | AE_IDLE_DMAWRITE)) == 0) break; DELAY(100); } sys/dev/netif/ae/if_ae.c:324 ae_stop_txmac(): same i-- shape (AE_IDLE_TXMAC | AE_IDLE_DMAREAD). sys/dev/netif/ae/if_aevar.h:41 AE_IDLE_TIMEOUT = 100 (intended 10 ms). i starts at 0 and only decrements, so (i < 100) is always true; the only exit is the device clearing the IDLE bits. The post-loop check if (i == AE_IDLE_TIMEOUT) (:294/:330) is unreachable β€” the "timed out while stopping Rx/Tx MAC." message can never print. Contrast the two correct sibling loops in the same file: ae_stop() :255 and ae_reset() :1510 both use i++.

TRIGGER / THREAT: AE_READ_4 on a surprise-removed, halted, D3-transitioned or errored device returns 0xFFFFFFFF β†’ IDLE bits stay set β†’ infinite busy-wait inside DELAY(100). A malicious/errant passthrough NIC can simply never clear them. Every caller of ae_stop()/ae_stop_{r,t}xmac() is a trigger: ifconfig down/up (ae_ioctl :1220-1221), ae_init :799 (also reached from ae_intr's DMAR/DMAW-timeout/PHY-linkdown path :770-775 in INTERRUPT CONTEXT), ae_watchdog :983, ae_detach :1417, ae_suspend :1786, ae_shutdown :1801, ae_miibus_statchg :366-367. Impact: the calling thread (or interrupt thread) spins forever holding ifp->if_serializer at kernel priority β€” interface + caller wedge until reboot (local DoS; no memory corruption, no privilege path).

EXPLOIT CHAIN: n/a (DoS only). On real hardware: fault/remove/halt the device (or passthrough a crafted one), then ifconfig ae0 down β†’ the ifconfig syscall never returns; one CPU pegged in DELAY.

EVIDENCE: stoploop.c exact-shape termination model (bugged + fixed variants) run.log guest run, RC_4217=0: 10,000,000 capped iterations in [1], exactly 100 in [2]; timeout branch shown unreachable (i=-9999999 != 100) env.txt no 1969:xxxx device; guest src md5 == audited tree fix.diff i-- -> i++ at :288 and :324 (git-apply clean)

POC CHANGES vs seed: none β€” no seed PoC existed; authored from scratch.

VERDICT: source-certain control-flow bug (loop counter decrements), local DoS only, 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 loop-termination defect proven by the model (unbounded as-written, exactly-100-reads fixed). The ae-specific fix path cannot be runtime-validated (no Attansic L2 hardware on the guest); fix.diff is a two-character mechanical change per site, git-apply clean against the audited tree, restoring the exact bounded shape of the correct sibling loops ae_stop():255 / ae_reset():1510.

fix.diff; run.log [1] vs [2] contrast (unbounded vs exactly 100 reads)
↓ fix.diffper-fix-DF-4217

Confirmed kernel references

Detail

Exploit chain

On real hardware: device enters non-idle state (fault, surprise removal, D3 glitch, or crafted passthrough NIC holding AE_IDLE_* bits set) -> ifconfig ae0 down (or intr-error reinit / suspend / detach) -> ae_stop_rxmac/ae_stop_txmac loop never exits -> calling thread (or interrupt thread) spins forever holding ifp->if_serializer; one CPU pegged, interface dead until reboot. No privilege path.

Evidence (decisive lines)

run.log (guest: [1] 10,000,000 capped iterations, timeout branch NOT taken, i=-9999999; [2] fixed variant exactly 100 reads; RC_4217=0), stoploop.c (exact-shape model), env.txt (NO_ATTANSIC_PCI_1969, if_ae not in kernel config, guest-src md5 match), fix.diff (i-- -> i++ at :288/:324, git-apply clean)

PoC changes

No seed PoC. Authored stoploop.c from scratch (exact loop-shape termination model, bugged + fixed variants); built and executed on the audit guest.

Verified recommended fix

Change i-- to i++ in both idle-wait loops (ae_stop_rxmac if_ae.c:288, ae_stop_txmac if_ae.c:324) β€” see fix.diff.

Verdict

Source-certain, runtime hardware-gated (inconclusive per family convention). ae_stop_rxmac() (if_ae.c:288) and ae_stop_txmac() (:324) wait for device idle with for (i = 0; i < AE_IDLE_TIMEOUT; i--) β€” the counter only decrements from 0, so the guard (i<100) is permanently true; unless the device reports idle the kernel thread busy-waits in DELAY(100) forever holding ifp->if_serializer, and the post-loop timeout branch (i == AE_IDLE_TIMEOUT) is unreachable (i can never be 100 again). Sibling loops ae_stop():255 / ae_reset():1510 correctly use i++. Dead/halted/surprise-removed device (reads 0xFFFFFFFF) or malicious passthrough NIC => permanent wedge of every stop path (ifconfig down :1221, init-from-intr :799/:770, watchdog :983, detach :1417, suspend :1786, shutdown :1801, statchg :366). Termination model proven on the audit guest (run.log RC=0: as-written shape unbounded β€” 10M-iteration self-cap hit, i=-9999999, timeout branch never taken; fixed i++ exits at exactly 100 reads). End-to-end trigger hardware-gated: no 1969:2048 on guest, if_ae not in X86_64_GENERIC, guest src md5 == audited tree. DoS only.