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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4589 Β· 11 files
FileTypeDescriptionSize
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)

  1. Host with a radeon (AMD) GPU whose connector is driven via radeon_dp_aux_transfer_atom (pre-DCE5, or DCE5 without radeon_auxch=1).
  2. Attach a DP dongle whose DPCD link-status replies follow trigger.c (never CR_DONE; alternate ADJUST_REQUEST voltage 0/1; DPCD[0x0E]=0).
  3. Trigger a modeset on the connector (hotplug, or xrandr --auto).
  4. 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.

VERDICT.md
↓ download raw

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:

  1. drm_dp_dpcd_read_link_status(...) <= 0 β€” atombios_dp.c:687-691 (sink/HW failure only)
  2. drm_dp_clock_recovery_ok(...) β€” :693-696 (sink achieves CR)
  3. all lanes DP_TRAIN_MAX_SWING_REACHED β€” :698-705 (requires voltage level 3)
  4. 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.mutex is 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_testable
baseline no→ patch + rebuild →patched clean

Fix 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
↓ fix.diffper-fix-DF-4589

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.