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

amdgpu_atombios_dp_link_train_cr has no overall iteration bound: malicious DP sink oscillating adjust-request voltage wedges the modeset path forever (unkillable task + mode_config.mutex) β€” line-for-line twin of DF-4589 (radeon)

Summary

The CR while(1)'s four exits are ALL sink-controlled, and the tries counter resets on ANY voltage change - a spec-violating sink alternating its ADJUST_REQUEST voltage while never reporting CR_DONE defeats every terminator; each iteration sleeps >=100us (up to ~508ms via crafted DPCD[0x0E]). Runs in the modeset path holding mode_config.mutex: unkillable task plus every subsequent DRM ioctl blocks forever - permanent local physical DoS of the display subsystem. Hardware-gated on guest; trigger compile-checked. Max-iteration-cap fix (mirrors DF-4589).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4652 Β· 12 files
FileTypeDescriptionSize
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 5.1 KB ↓ raw
trigger.c β€” 5.6 KB view raw
build.sh β€” 169 B view raw
run.sh β€” 175 B view raw
build.log β€” 44 B view raw
run.log β€” 321 B view raw
guest_probe.log β€” 321 B view raw
env.txt β€” 820 B view raw
fix.diff β€” 575 B view raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 3.7 KB view raw

DF-4652 β€” amdgpu CR link-training loop has no overall iteration bound

Finding file: sys/dev/drm/amd/amdgpu/atombios_dp.c (pass 2) Line span: 605-644 (clock-recovery loop), decisive: 619-635. Twin of DF-4589 (radeon, sys/dev/drm/radeon/atombios_dp.c:684-722) β€” the two loops are line-for-line identical in shape.

What the bug is

amdgpu_atombios_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:634-635). 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:685 with unconditional increment at :695); only the CR loop is unbounded.

Impact

amdgpu_atombios_dp_link_train() (atombios_dp.c:710) runs from amdgpu_atombios_encoder_setup_dig() (atombios_encoders.c:1441, ATOM_ENABLE branch), invoked from amdgpu_atombios_encoder_dpms() (atombios_encoders.c:1477) β€” the .dpms encoder-helper callback registered by dce_v6/v8/v10/v11 (e.g. dce_v10_0.c:3421). That is the DRM modeset path taken with dev->mode_config.mutex held (drm_crtc.c:601/776, drm_crtc_helper.c:101/139) β€” either the HPD workqueue thread or the userspace thread that issued MODE_SETCRTC / a DPMS property change. 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 / DF-4589. No memory corruption (lane counts are handled under DF-1790's separate finding).

Reproduce (hardware required)

  1. Host with an amdgpu (AMD) GPU driven through the atom BIOS table path (amdgpu_atombios_dp_process_aux_ch).
  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 amdgpu_atombios_dp_link_train_cr; dmesg shows neither "clock recovery tried 5 times" nor "clock recovery failed"; further DRM ioctls block. PATCHED: loop exits after the hard cap with "clock recovery exceeded max loop attempts" + "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

./build.sh       # compiles the documented sink state machine
./run.sh         # probes guest hardware; records absence in run.log

Fix

fix.diff adds an overall iteration cap (50) to the CR loop, mirroring the DF-4589 fix for the radeon twin.

VERDICT.md
↓ download raw

DF-4652 VERDICT β€” amdgpu clock-recovery loop unbounded (DF-4589 twin)

STATUS: not_reproduced (hardware-gated). Defect certain from source. IMPACT ceiling: unkillable modeset task holding mode_config.mutex -> permanent display-subsystem wedge (local physical DoS). No memory corruption.

Reproduced?

NO β€” on this guest it CANNOT run: reproduction needs an AMD GPU driven by the amdgpu atom path plus a crafted DisplayPort sink. The audit guest has no 0x1002 PCI device at all (Bochs VGA 0x1234:0x1111 β€” run.log, env.txt), and QEMU cannot emulate a malicious DP sink behind atom ProcessAuxChannelTransaction. This mirrors DF-4589 (radeon twin), which was hardware-gated identically.

Why the defect is certain from source

amdgpu_atombios_dp_link_train_cr() (sys/dev/drm/amd/amdgpu/ atombios_dp.c:588-655) is a while (1) loop whose four exits are ALL sink-controlled:

  1. aux read failure β€” :608-612 (sink answers fine -> never taken)
  2. CR achieved β€” :614-617 (sink never sets CR_DONE -> never taken)
  3. all lanes at MAX_SWING_REACHED β€” :619-626 get_adjust_train() (:629-233 / :206-241) sets DP_TRAIN_MAX_SWING_REACHED only when the sink's requested voltage reaches level 3 (v >= DP_VOLTAGE_MAX, :229-230). A sink that always requests level 0/1 keeps this exit closed forever.
  4. 5 consecutive identical voltages β€” :628-635 if ((train_set[0] & DP_TRAIN_VOLTAGE_SWING_MASK) == voltage) ++tries; if (tries == 5) break; else tries = 0; <-- THE DEFECT (:634-635)

A sink that ALTERNATES its ADJUST_REQUEST voltage between level 0 and level 1 on every link-status read makes the equality test fail on every other iteration, resetting tries to 0 forever. The loop is infinite: each iteration sleeps >= 100 us (up to mdelay(rd_interval*4) with a crafted DPCD[0x0E], drm_dp_helper.c:121-134) and performs one DPCD read via the atom AUX channel β€” slow-spin, never exit. Full iteration trace table in trigger.c.

Twin check (the reason this finding exists): the radeon twin radeon_dp_link_train_cr() (sys/dev/drm/radeon/atombios_dp.c:684-722, DF-4589) is line-for-line identical in shape: same while (1), same four exits, same else dp_info->tries = 0; at :713-714. The amdgpu copy is the same defect; DF-4589's analysis carries over verbatim.

Contrast: the channel-equalization loop in the same file IS bounded β€” tries is incremented unconditionally every iteration and checked with > 5 (atombios_dp.c:685, :695), exiting after at most 7 iterations. Only the CR loop is unbounded.

Caller context / why it wedges the whole display subsystem

amdgpu_atombios_dp_link_train_cr <- amdgpu_atombios_dp_link_train() atombios_dp.c:710,755 <- amdgpu_atombios_encoder_setup_dig(ATOM_ENABLE) atombios_encoders.c:1441 <- amdgpu_atombios_encoder_dpms() atombios_encoders.c:1477 <- .dpms encoder-helper callbacks of dce_v6/v8/v10/v11 (e.g. dce_v10_0.c:3421) <- drm_helper_encoder_dpms / modeset-commit paths taken with dev->mode_config.mutex held (drm_crtc.c:601/776; drm_crtc_helper.c:101/139)

So the spinning task is either the HPD workqueue thread or the user thread that issued MODE_SETCRTC / a DPMS property change. Result: an unkillable task (non-interruptible kernel loop) and every subsequent DRM ioctl on the card blocking on mode_config.mutex. Precondition: a crafted DP sink (MCU dongle) β€” the physical malicious-peripheral threat model already accepted for DF-1730 / DF-1778 / DF-4589. Not reachable from pure software input; no memory corruption (the >4-lane DPCD issue is a separate, known finding: DF-1790).

What I ran (attempts)

  1. Guest probe: no AMD GPU present β€” hardware-gated (run.log, guest_probe.log, env.txt). DONE.
  2. Compile-check of the documented sink state machine on the guest: cc -Wall -Wextra -O2 clean (build.log).
  3. Source trace of every exit condition and the caller chain (above); verified the CE loop is bounded; verified the radeon twin is line-identical (diff by eye against DF-4589's cited lines).

Fix validation

fix.diff adds an overall iteration cap (50) to the CR loop β€” the same fix shape as DF-4589's. git apply --check passes against the unmodified sys/ tree. Baseline cannot run on the guest (no amdgpu hardware), so the patched behavior ("clock recovery exceeded max loop attempts" then "clock recovery failed", modeset unwinds normally) is validated by inspection: the cap is checked at the top of every iteration and exits unconditionally.

Recommended fix: hard iteration cap on the CR loop (see fix.diff).

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 at runtime on guest: baseline cannot run (no amdgpu GPU). fix.diff applies cleanly (git apply --check) against atombios_dp.c:591-606 and bounds the loop unconditionally with a top-of-loop counter check; validated by inspection.

findings/poc/DF-4652/fix.diff
↓ fix.diffper-fix-DF-4652

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (exit-condition enumeration + caller-chain proof + twin diff); trigger.c (deterministic oscillating-sink state machine with iteration trace table); build.log (guest cc -Wall -Wextra -O2 compile clean); run.log/guest_probe.log/env.txt (no 0x1002 device on guest); fix.diff (50-iteration hard cap, git apply --check clean)

PoC changes

No seed pack existed; trigger authored as a documented malicious-sink state machine mirroring DF-4589's pack (guest DragonFly cc compile-check passed, zero warnings; runtime needs amdgpu HW + crafted DP dongle).

Verified recommended fix

Add an overall iteration cap (50) to the CR loop so a spec-violating sink cannot wedge the modeset path (see fix.diff).

Verdict

amdgpu_atombios_dp_link_train_cr() (sys/dev/drm/amd/amdgpu/atombios_dp.c:605-644) has no overall iteration bound β€” it is the line-for-line twin of the radeon loop filed as DF-4589: all four exits are sink-controlled and the 5-try counter is reset to 0 on ANY voltage change (atombios_dp.c:634-635), 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 chain (atombios_encoders.c:1441 -> amdgpu_atombios_encoder_dpms :1477 -> dce_v6/8/10/11 .dpms) runs in the DRM modeset path holding mode_config.mutex (drm_crtc.c:601/776), 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:685/695). Realistic impact ceiling: local physical DoS via malicious DP sink (same threat model as DF-4589/DF-1778/DF-1730), no memory corruption.