AUX completion poll off-by-one: a DisplayPort AUX transaction that completes on the 1001st poll is misreported as -EIO (counter-keyed timeout gate)
Summary
do-while runs the body with retry_count==1000 and the gate tests the counter, failing a transfer whose DONE landed in the last poll - transient DP aux failure self-healing via retries (no memory safety). Aggravations: DF usleep_range maps to DELAY(100) so a wedged AUX busy-waits ~100ms per transaction under chan->mutex; error paths skip the DONE_ACK (mitigated by the next call's unconditional re-ack). Executed control-flow proof on guest; status-keyed gate fix harness-verified.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5148 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.4 KB | β raw | |
| VERDICT.md | β | 4.2 KB | β raw | |
| loop_offbyone.c | β | 3.7 KB | view raw | |
| build.sh | β | 66 B | view raw | |
| run.sh | β | 90 B | view raw | |
| build.log | β | 43 B | view raw | |
| run.log | β | 627 B | view raw | |
| env.txt | β | 5.9 KB | view raw | |
| fix.diff | β | 531 B | view raw | |
| manifest.json | β | 1.0 KB | view raw | |
| verdict.json | β | 4.2 KB | view raw |
DF-5148 β AUX completion poll off-by-one misreports a completed transfer as -EIO
File: sys/dev/drm/radeon/radeon_dp_auxch.c
Severity: Info (correctness; no memory-safety impact)
Status: untested (control-flow arithmetic proved by executed harness; on-HW
reproduction impossible in the audit guest β no AMD/ATI PCI device, QEMU
emulated VGA only, so radeon_dp_aux_transfer_native() can never attach/run
there; see env.txt)
What
radeon_dp_aux_transfer_native() polls the AUX_SW_STATUS register for
AUX_SW_DONE (radeon_dp_auxch.c:152-158):
int retry_count = 0;
do {
tmp = RREG32(AUX_SW_STATUS + aux_offset[instance]);
if (tmp & AUX_SW_DONE) {
break;
}
usleep_range(100, 200);
} while (retry_count++ < 1000);
if (retry_count >= 1000) { /* keyed on the COUNTER */
DRM_ERROR("auxch hw never signalled completion, error %08x\n", tmp);
ret = -EIO;
goto done;
}
The do-while executes the body with retry_count == 0..1000 (1001 body
executions): after the body entered with retry_count == 999, the guard
retry_count++ < 1000 increments to 1000 and re-enters the body one last
time. If the hardware asserts AUX_SW_DONE during that 1001st poll, the loop
breaks with retry_count == 1000 β and the counter-keyed error gate then
reports a completed transaction as -EIO ("auxch hw never signalled
completion"). The correct signal is available in tmp at that point; the
gate just tests the wrong variable.
Secondary aggravations (same loop, noted for completeness):
* DFBSD's usleep_range(100,200) maps to DELAY(100) (bounded busy-wait,
sys/dev/drm/include/linux/delay.h:55-58), so a wedged AUX block busy-waits
up to ~100 ms per transaction with chan->mutex held (locked at
radeon_dp_auxch.c:96) β 100x upstream Linux's udelay(1)-based ~1 ms.
Bounded, but a latency wart under retry storms.
* The error goto done paths (lines 163/168/174) skip the second
AUX_SW_DONE_ACK clear at line 194; stale DONE state is however re-acked
by the unconditional write at line 145 of the next call before GO is
set, so this is cosmetic.
Impact
A DisplayPort AUX transaction that completes on the final poll iteration is spuriously failed with -EIO. Upper layers (drm_dp_aux / DPCD accessors / link training) retry aux transactions, so the practical effect is a rare transient failure (EDI D/EDID read, DPCD access, link training step), not a memory-safety issue. Info severity.
Related known findings (pass 1, NOT part of this one): DF-2146 (unbounded
AUX_SW_REPLY byte count vs 16-byte buffer), DF-2147 (unguarded
aux_offset[instance]).
Reproduce (control-flow proof)
./build.sh # compiles loop_offbyone.c on the audit QEMU guest (or any POSIX cc) ./run.sh # runs the stimulus table
Expected decisive output (see run.log):
DONE on 1001st poll (rc=1000) <-- last body -5 0 0 BUG: completed xfer misreported as -EIO never DONE (true HW timeout) -5 -5 -5 RESULT: shipped loop misclassifies a DONE-on-last-poll transfer as -EIO (radeon_dp_auxch.c:160).
The "fixed gate" column shows the proposed if (!(tmp & AUX_SW_DONE)) gate
behaves identically to the shipped gate for every other stimulus, i.e. the
fix only flips the misclassified case.
Fix
fix.diff β gate the timeout on the observed status word, not the counter.
```
VERDICT β DF-5148
Verdict: real code defect, certain from the control flow; Info-class
correctness bug with no security impact. PoC status untested β the audit
guest has no radeon GPU (env.txt: pciconf -l shows no AMD/ATI device,
QEMU emulated VGA only), so radeon_dp_aux_transfer_native() cannot execute
on real hardware there; per contract, Info-class findings skip QEMU
verification. The loop arithmetic itself was executed and proved on the
guest with loop_offbyone.c (run.log): a DONE asserted on the 1001st poll
body exits the loop with retry_count == 1000 and the counter-keyed gate at
sys/dev/drm/radeon/radeon_dp_auxch.c:160 returns -EIO for a completed
transfer, while the proposed status-keyed gate returns success; every other
stimulus (DONE at polls 1, 501, 1000; never) is classified identically by
both gates.
Why it happens (path:line)
sys/dev/drm/radeon/radeon_dp_auxch.c:66βint retry_count = 0;:152-158βdo { tmp = RREG32(AUX_SW_STATUS...); if (tmp & AUX_SW_DONE) break; usleep_range(100,200); } while (retry_count++ < 1000);The guard is evaluated after the body, so the body runs withretry_count == 0..1000inclusive (1001 executions; noteretry_countis only incremented in the guard).:160-164βif (retry_count >= 1000) { DRM_ERROR("auxch hw never signalled completion..."); ret = -EIO; goto done; }β a DONE observed in the body entered withretry_count == 1000breaks the loop with the counter at exactly 1000, so a successful completion is misclassified.- Aggravation:
sys/dev/drm/include/linux/delay.h:55-58mapsusleep_range(min,max)toDELAY(min)(busy-wait), so the poll spins ~100 Β΅s per iteration, up to ~100 ms per failed transaction while holdingchan->mutex(locked at:96, released at:199). Upstream Linux usesudelay(1)(~1 ms worst case). Bounded β no infinite wait exists in this file.
Why Info and not higher
- No memory-safety consequence: the misclassified path takes the same
goto donecleanup (mutex release) that real timeouts take;tmp,ack,bufare untouched. - The wrong result surfaces as
-EIOto drm_dp_aux callers, which own retry/nack handling; effect is a rare transient DP aux failure. - Trigger requires real DisplayPort hardware completing the transaction in the ~100 ms window's final poll.
Fix validation
fix.diff replaces the counter test with !(tmp & AUX_SW_DONE). Not
boot-tested (fix_status: not_testable): requires radeon+DP hardware;
the harness proves behavioral equivalence for all non-buggy stimuli and the
correct classification for the buggy one. sys/ remains untouched
(diff authored read-only).
Negative results recorded during this pass-2 review (classes hunted, killed)
- Waits/timeouts: the completion poll is the ONLY loop besides the two
bounded for-loops (
i < msg->size <= 16at :138; the reply loop is known DF-2146). No unbounded/forever wait in this file. - NACK/retry handling: absent by design here (ack nibble propagated at :201-202 to the drm_dp_aux core, which owns retries).
- Native vs I2C bridge: request dispatch (:74-84) masks MOT and accepts only
the 4 canonical request types, else -EINVAL;
msg->size > 16rejected at :71-72 (WARN_ON + -E2BIG);msize = size-1fits u8; write loop bounded by the checked size. - Register field overflow:
AUX_SW_WR_BYTES(x)=((x&0x1f)<<16)(nid.h:840) self-masks, bytes <= 20 fits;AUX_HPD_SEL(x)=((x&0x7)<<20)(nid.h:832) self-masks andrec.hpdis aradeon_hpd_idenum (<= RADEON_HPD_6), and the field is pre-cleared viatmp &= AUX_HPD_SEL(0x7)at :106. buf == NULLhandled at :187/:190; mutex held across the whole MMIO sequence (:96-:199).- DF-2146 (
:40,:177-191reply byte count up to 31 vs <=16-byte buffer) and DF-2147 (:44-52,:63aux_offset[0..15] vs 6 entries) are the known pass-1 findings and are deliberately not re-reported here.
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_testablefix.diff not boot-tested: validating it requires radeon DisplayPort hardware timing a completion into the final poll iteration, which the audit guest cannot provide (no AMD/ATI PCI device). The harness proves the status-keyed gate classifies every stimulus correctly (fixed column of run.log), including the case the shipped gate gets wrong; the diff is a one-line, behavior-preserving-for-all-other-paths change, git-apply --check verified against the pristine sys/ tree.
run.log 'fixed gate' column: 0/0/0/0/-5 vs shipped 0/0/0/-5/-5; fix.diff hunk @@ -157,7 +157,7 @@ replacing the counter test with !(tmp & AUX_SW_DONE)
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log lines 3-6: 'DONE on 1001st poll (rc=1000) <-- last body -5 0 0 BUG: completed xfer misreported as -EIO' and 'RESULT: shipped loop misclassifies a DONE-on-last-poll transfer as -EIO (radeon_dp_auxch.c:160)'; VERDICT.md traces the loop semantics; env.txt proves guest lacks radeon HW (no AMD/ATI PCI device)
PoC changes
Harness authored fresh (no seed existed for this finding); no guest kernel changes; userspace-only compile on guest cc 8.3
Verified recommended fix
Gate the timeout on the observed status word, not the iteration counter: replace 'if (retry_count >= 1000)' with 'if (!(tmp & AUX_SW_DONE))' (see fix.diff, git-apply --check verified against pristine sys/)
Verdict
Real, certain-from-code-shape off-by-one in the AUX completion poll of radeon_dp_aux_transfer_native(): the do-while at radeon_dp_auxch.c:152-158 executes its body with retry_count==0..1000 (1001 executions), so a hardware AUX_SW_DONE observed during the 1001st poll breaks the loop with retry_count==1000 and the counter-keyed gate at :160 then fails a COMPLETED transfer with -EIO ('auxch hw never signalled completion'). The control-flow arithmetic was executed and proved on the audit guest with loop_offbyone.c (run.log: shipped gate -5/EIO vs fixed gate 0 for DONE-at-rc=1000; identical classification for all other stimuli). On-hardware verification is impossible in the audit guest: no AMD/ATI PCI device exists there (env.txt: pciconf shows QEMU emulated VGA only), so the radeon driver cannot attach and the MMIO poll cannot run; per contract Info-class findings skip QEMU Phase V - hence status 'untested', not a refutation. No memory-safety impact: the misclassified path takes the normal 'goto done' cleanup; callers (drm_dp_aux core) retry aux transactions, so the effect is a rare transient DP aux failure. Related known pass-1 findings DF-2146/DF-2147 cover the reply-count OOB and the aux_offset index and are not part of this finding.
No comments yet.