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)
PoC verification
Evidence pack
findings/poc/DF-4217 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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):
- As written (
i--): the guardi < 100is 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-loopif (i == AE_IDLE_TIMEOUT)timeout branch is unreachable (i = -9999999 β 100, and it can never rise to 100 again). - 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).
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_testableBaseline 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)
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.
No comments yet.