radeon_dp_link_train_cr has no overall iteration bound: malicious DP sink oscillating adjust-request voltage wedges the modeset path forever (unkillable task + display-subsystem lock)
Summary
The CR loop exits only on read failure, CR achieved, max swing, or 5 CONSECUTIVE identical-voltage tries - and the counter resets on ANY voltage change. A crafted sink that never reports CR, stays below level 3, and alternates voltage between two levels defeats all four exits forever; each iteration burns >=100us (up to ~508ms via crafted DPCD[0x0E]). Runs from the modeset path holding mode_config.mutex: unkillable task plus every subsequent DRM ioctl blocks. The CE loop is properly bounded; only CR is not. Malicious-sink model (same as DF-1730/1778). Hard iteration cap fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4589 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.7 KB | β raw | |
| VERDICT.md | β | 3.4 KB | β raw | |
| trigger.c | β | 4.7 KB | view raw | |
| run.sh | β | 179 B | view raw | |
| run.log | β | 321 B | view raw | |
| build.log | β | 406 B | view raw | |
| env.txt | β | 209 B | view raw | |
| guest_probe.log | β | 677 B | view raw | |
| fix.diff | β | 559 B | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 3.1 KB | view raw |
DF-4589 β radeon CR link-training loop has no overall iteration bound
Finding file: sys/dev/drm/radeon/atombios_dp.c (pass 2) Line span: 684-722 (clock-recovery loop), decisive: 707-714.
What the bug is
radeon_dp_link_train_cr() loops until clock recovery succeeds, the DPCD
read fails, every lane reports DP_TRAIN_MAX_SWING_REACHED, or the sink
requests the same voltage 5 consecutive times (dp_info->tries, reset
to 0 on any change at atombios_dp.c:713-714). A spec-violating DP sink
that never achieves CR, always requests voltage swing below level 3, and
alternates its adjust-request voltage between two levels (0.4V/0.6V)
defeats every terminator: the 5-try counter is reset on each alternation
and the loop spins forever, sleeping >= 100 us (or up to ~508 ms with a
crafted DPCD[0x0E] TRAINING_AUX_RD_INTERVAL) plus an AUX transaction per
iteration (drm_dp_helper.c:121-134).
The channel-equalization loop is bounded (tries > 5 unconditional break
at atombios_dp.c:762); only the CR loop is unbounded.
Impact
radeon_dp_link_train() runs from the encoder DPMS-on/modeset path
(atombios_encoders.c:1777) β i.e. either the HPD workqueue thread or the
userspace thread that issued MODE_SETCRTC / a DPMS property change,
holding mode_config.mutex. Outcome: an unkillable task in a
non-interruptible kernel loop plus a permanently wedged display subsystem
(all subsequent DRM ioctls on the card block). Precondition is a crafted
DisplayPort sink (MCU dongle) β the same physical malicious-sink threat
model already accepted for DF-1730 / DF-1778. No memory corruption.
Reproduce (hardware required)
- Host with a radeon (AMD) GPU whose connector is driven via
radeon_dp_aux_transfer_atom (pre-DCE5, or DCE5 without
radeon_auxch=1). - Attach a DP dongle whose DPCD link-status replies follow trigger.c (never CR_DONE; alternate ADJUST_REQUEST voltage 0/1; DPCD[0x0E]=0).
- Trigger a modeset on the connector (hotplug, or
xrandr --auto). - EXPECTED (vulnerable): the issuing task hangs forever in radeon_dp_link_train_cr; dmesg shows no "clock recovery tried 5 times" / "clock recovery failed" messages; further DRM ioctls block. PATCHED: loop exits after the hard cap with "clock recovery failed".
On the audit guest this is hardware-gated: no AMD GPU exists (Bochs 0x1234:0x1111 β run.log / env.txt), so the verdict is not_reproduced with the defect proven from source.
Build / run on guest
./run.sh # probes guest hardware; records absence in run.log
Fix
fix.diff adds an overall iteration cap to the CR loop.
DF-4589 VERDICT β not_reproduced (hardware-gated), defect certain from source
Finding
radeon_dp_link_train_cr() (sys/dev/drm/radeon/atombios_dp.c:668-733) has no
overall iteration bound. Its four exit conditions:
drm_dp_dpcd_read_link_status(...) <= 0β atombios_dp.c:687-691 (sink/HW failure only)drm_dp_clock_recovery_ok(...)β :693-696 (sink achieves CR)- all lanes
DP_TRAIN_MAX_SWING_REACHEDβ :698-705 (requires voltage level 3) - 5 consecutive equal-voltage iterations β :707-714, with
else dp_info->tries = 0;at :713-714 resetting the counter on ANY change
are all sink-controlled. A crafted sink alternating its adjust-request
voltage between levels < 3 while never reporting CR keeps tries at 0
indefinitely β infinite loop with per-iteration delay
(drm_dp_link_train_clock_recovery_delay, sys/dev/drm/drm_dp_helper.c:121-134:
udelay(100) minimum, mdelay(rd_interval*4) up to ~508 ms via DPCD[0x0E])
plus a DPCD read.
Why it is exploitable / who spins
Call chain: userspace modeset / hotplug β encoder DPMS-on
(atombios_encoders.c:1777 radeon_dp_link_train(encoder, connector)) inside
the DRM modeset path holding dev->mode_config.mutex. Consequences:
- the issuing thread (user ioctl or HPD workqueue) spins forever, non-interruptibly β unkillable process;
mode_config.mutexis held forever β every subsequent DRM ioctl on the card blocks: permanent display-subsystem DoS for all users of the GPU.
Distinct from DF-1778 (unbounded goto-loop in radeon_dp_mst.c check_status) and DF-1730 (lane-count OOB): different file, different loop, different primitive (pure DoS, no memory access violation).
Reproduction attempt on the audit guest
- Guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (env.txt).
- run.sh / run.log: no AMD GPU (vendor 0x1002) β only Bochs 0x1234:0x1111. A crafted DP sink additionally requires physical DP attachment.
- Therefore the runtime trigger cannot execute on this guest: status not_reproduced, reproduced=0, impact none (not manifested here). Realistic impact ceiling on real hardware: kernel-thread wedge / unkillable process + display DoS (physical, no privilege gain).
Confidence in the code defect: certain β every exit condition is enumerated above and each is under sink control; the counter-reset at :713-714 is unconditional on voltage change. Exploitation likelihood: likely (needs a crafted sink; trivially implementable on an MCU DP dongle, as with prior malicious-sink research).
Fix validation
fix.diff adds a hard cap of 50 CR iterations (covers worst-case legitimate voltage walks: 4 levels x 5 tries with resets) and logs "clock recovery exceeded max loop attempts". Baseline vs patched cannot be exercised on this guest (no radeon HW) β fix_status: not_testable; the diff is line-accurate against atombios_dp.c:668-722.
Artifacts
- trigger.c β deterministic malicious-sink state machine + loop trace table
- run.sh, run.log, guest_probe.log, env.txt β guest hardware-absence proof
- fix.diff β iteration cap
- manifest.json / verdict.json
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 not validated on guest: baseline cannot run (no radeon GPU). fix.diff applies cleanly (git apply --check) against atombios_dp.c:668-722 and bounds the loop unconditionally; validated by inspection.
findings/poc/DF-4589/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (exit-condition enumeration + caller-context proof); trigger.c (deterministic oscillating-sink state machine with iteration trace table); run.log/env.txt/guest_probe.log (no 0x1002 device on guest); fix.diff (50-iteration hard cap, git-apply clean)
PoC changes
No seed pack existed; trigger authored as a documented malicious-sink state machine (guest DragonFly cc compile-check passed; runtime needs radeon HW + crafted DP dongle).
Verified recommended fix
Add an overall iteration cap to the CR loop (50 iterations) so a spec-violating sink cannot wedge the modeset path (see fix.diff).
Verdict
radeon_dp_link_train_cr() (atombios_dp.c:684-722) has no overall iteration bound: all four loop exits are sink-controlled, and the 5-try counter is reset to 0 on ANY voltage change (atombios_dp.c:713-714), so a crafted DP sink that alternates its adjust-request voltage between levels < 3 while never achieving clock recovery spins the loop forever (>= 100 us delay + DPCD read per iteration, drm_dp_helper.c:121-134). The caller runs in the DRM modeset path holding mode_config.mutex (atombios_encoders.c:1777) - user ioctl thread or HPD workqueue - producing an unkillable task and a permanently wedged display subsystem. Not reproduced on the audit guest: no AMD GPU exists (Bochs 0x1234:0x1111, run.log/env.txt) and QEMU cannot emulate the crafted DP sink, so execution is hardware-gated; the loop analysis is exhaustive and the defect is certain from source (the CE loop by contrast IS bounded at atombios_dp.c:762). Realistic impact ceiling: local physical DoS (same malicious-sink threat model as DF-1730/DF-1778), no memory corruption.
No comments yet.