Poisoned cur_link_settings.lane_count outlives link training: dp_test_send_phy_test_pattern OOB stack WRITE past lane_settings[4] (~3KB span) and hpd_rx_irq_check_link_loss_status OOB stack READ past the 6-byte union hpd_irq_data
Summary
dp_test_send_link_training reads the raw sink byte DP_TEST_LANE_COUNT (0..255, unvalidated) and persists it into verified_link_cap and cur_link_settings (via dp_retrain_link_dp_test), where it outlives training. Sink A: dp_test_send_phy_test_pattern writes up to 3060 bytes past lane_settings[4] (deterministic stack smash; values 0..3 => panic, not controlled RIP). Sink B: hpd_rx_irq_check_link_loss_status reads up to 122B past the 6-byte union hpd_irq_data. Both phases run in one HPD IRQ via handle_automated_test. NOT a re-report of DF-1319 (that was the transient lt_settings overflow; this is the persisted poisoning reaching different functions/buffers/lifetimes).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3987 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger | hardware-precondition model / protocol replica | 5.6 KB | view raw |
| README.md | readme | build/run/expected | 1.0 KB | β raw |
| VERDICT.md | verdict | full path:line trace | 5.0 KB | β raw |
| build.sh | script | exact build commands | 65 B | view raw |
| run.sh | script | exact run commands | 89 B | view raw |
| build.log | log | full untrimmed build output | 298 B | view raw |
| run.log | log | full untrimmed runtime output | 2.4 KB | view raw |
| env.txt | env | guest hardware/src-md5 proof | 1.1 KB | view raw |
| fix.diff | fix | git-apply-able unified diff | 2.5 KB | view raw |
DF-3987 β poisoned cur_link_settings.lane_count (dp_test_send_link_training) persists after retrain: dp_test_send_phy_test_pattern writes lane_settings[lane] for lane < 255 β up to ~3012 bytes past the 48-byte struct dc_link_training_settings.lane_settings[4] (stack SMASH), and hpd_rx_irq_check_link_loss_status nibble-reads up to 122 bytes past the 6-byte union hpd_irq_data. Distinct sinks from DF-1319 (which covers the dpcd_lt_buffer[5]/dpcd_lane[4] overflow inside the LT helpers).
BUILD cc -O2 -Wall -Wextra -o trigger trigger.c
RUN ./trigger (arithmetic model; prints OOB extents for every sink-fed lane_count class; RC 0)
On the audit guest (vm.sh run_root after scp): builds and runs clean β see run.log. Hardware-gated: guest has bochs VGA only, zero 0x1002 PCI devices, amdgpu.ko not loaded β the DP sink chain is unreachable.
EXPECTED For DP_TEST_LANE_COUNT in {5, 0x1f, 0x20, 0x7f, 0xff}: "OOB WRITE" lines with byte extents (12..3012 bytes) and the hpd "OOB READ" extents; TRIGGER_RC=0.
DF-3987 VERDICT β source-level certain, runtime hardware-gated (inconclusive)
What was audited (pass 2, excluding DF-1319)
sys/dev/drm/amd/display/dc/core/dc_link_dp.c second pass. DF-1319 (known)
covers: dp_test_send_link_training reads a raw DP_TEST_LANE_COUNT byte and
the transient lt_settings overflow it causes inside the link-training
helpers (dpcd_set_lt_pattern_and_lane_settings :168/:194/:212-218,
get_lane_status_and_drive_settings :432). This finding is about where that
same unvalidated byte goes AFTER link training β two sinks DF-1319's trace
never touches.
Root cause chain (path:line)
dp_test_send_link_trainingβ dc_link_dp.c:1772-1781 reads raw sink bytesDP_TEST_LANE_COUNT/DP_TEST_LINK_RATE(1 byte each, no validation againstLANE_COUNT_DP_MAX=4 β dc_dp_types.h:31-37).- :1784-1785 persists the byte into
link->verified_link_cap.{lane_count, link_rate}. - :1787
dp_retrain_link_dp_test(link, &link_settings, false)β dc_link_hwss.c:270-340 β performs the retrain (DF-1319's transient overflow fires here) and at :319 executeslink->cur_link_settings = *link_setting;β the raw byte now LIVES incur_link_settingsand survives until the next full mode set. handle_automated_test(dc_link_dp.c:1965-2010) processesDP_TEST_REQUESTbits sequentially in ONE HPD-RX-IRQ invocation: LINK_TRAINING (bit0) at :1978-1986 then PHY_TEST_PATTERN (bit2) at :1994-1995. A sink setting both bits poisonscur_link_settingsand immediately feeds it todp_test_send_phy_test_pattern.
New sink A β stack WRITE overflow (dp_test_send_phy_test_pattern)
dc_link_dp.c:1879-1895:
link_settings.link = link->cur_link_settings;
for (lane = 0; lane < (unsigned int)(link->cur_link_settings.lane_count); lane++) {
...
link_settings.lane_settings[lane].VOLTAGE_SWING = ... /* :1886 */
link_settings.lane_settings[lane].PRE_EMPHASIS = ... /* :1889 */
link_settings.lane_settings[lane].POST_CURSOR2 = ... /* :1892 */
}
struct dc_link_training_settings (dc_dp_types.h:102-105) has
lane_settings[LANE_COUNT_DP_MAX] = 4 Γ 12 bytes. With the persisted byte
0xFF the loop writes 255 Γ 12 = 3060 bytes into a 48-byte array on this
function's stack β ~3012 bytes of linear up-stack smash (saved frames,
return addresses). Values are constrained (each 4-byte enum word β 0..3 from
2-bit DPCD nibbles / POST_CURSOR2 bits), so return addresses become
non-canonical β deterministic kernel panic; controlled RIP is not reachable
with these values. Auxiliary OOB READ at :1885
(get_nibble_at_index(&dpcd_lane_adjustment[0].raw, lane) for lane up to
254 reads dpcd_lane_adjustment[lane/2], 2-element array) feeds the written
values from up to 126 bytes past the array.
New sink B β stack READ overflow (hpd_rx_irq_check_link_loss_status)
dc_link_dp.c:1596-1615 loops
lane < link->cur_link_settings.lane_count calling
get_nibble_at_index(&hpd_irq_dpcd_data->bytes.lane01_status.raw, lane)
(:1600) β buf[lane/2]. union hpd_irq_data is 6 bytes
(dc_dp_types.h:341-351). With the persisted byte 0xFF the loop reads index
127 β 122 bytes past the 6-byte caller-local (dc_link_handle_hpd_rx_irq
:2014). Branch-only use (link-loss decision) β garbage-driven retrain, no
direct disclosure, no fault (stack).
Also note :1590-1591: the only guard is lane_count == 0, not > 4.
Verification on the guest (honest)
- trigger.c (this pack) models the exact index/size arithmetic; builds and
runs on the guest (
run.log, TRIGGER_RC=0, extents for byte classes {5, 0x1F, 0x20, 0x7F, 0xFF}). - The real chain is hardware-gated on this guest:
pciconf -lvshows zero 0x1002 (AMD) devices (bochs VGA only),amdgpu.koexists in /boot/kernel but is NOT loaded (kldstat), so dc_link_dp.c code never executes. Status: inconclusive per family convention (DF-1319, DF-3759, DF-3604). - Guest /usr/src dc_link_dp.c md5
028326495b845611c38ec70dcd655bf3== audited tree (env.txt).
Threat model
Malicious DP sink / MST hub / test appliance (or a man-in-the-middle on an unencrypted DP AUX? β AUX is not remotely reachable; physical or dongle-borne). One short-HPD with AUTOMATED_TEST + LINK_TRAINING + PHY_TEST_PATTERN β stack smash β panic. Same threat model DF-1319 was filed under (High). Escalation beyond panic is implausible: written words β {0..3}.
Fix
fix.diff: (1) validate DP_TEST_LANE_COUNT β {1,2,4} and DP_TEST_LINK_RATE
against the known enum at the source (dp_test_send_link_training) β kills
this finding AND the DF-1319 entry path; (2) defensive lane <
LANE_COUNT_DP_MAX caps at both new sinks. git-apply clean against the
audited tree; not runtime-validable (no AMD GPU).
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 validates the sink bytes at the source (kills the poisoning for this finding AND the DF-1319 entry path) and defensively caps both new sinks' loops at LANE_COUNT_DP_MAX. Cannot be runtime-validated: guest has no AMD GPU, so neither baseline nor patched behavior is exercisable. Patch touches only existing symbols on already-compiled paths; git-apply clean against the audited tree.
fix.diff; hardware absence proof in env.txt (NO_AMD_GPU / amdgpu.ko unloaded)
Confirmed kernel references
- sys/dev/drm/amd/display/dc/core/dc_link_dp.c:1772
- sys/dev/drm/amd/display/dc/core/dc_link_dp.c:1784
- sys/dev/drm/amd/display/dc/core/dc_link_dp.c:1787
- sys/dev/drm/amd/display/dc/core/dc_link_hwss.c:319
- sys/dev/drm/amd/display/dc/core/dc_link_dp.c:1881
- sys/dev/drm/amd/display/dc/core/dc_link_dp.c:1886
- sys/dev/drm/amd/display/dc/core/dc_link_dp.c:1885
- sys/dev/drm/amd/display/dc/core/dc_link_dp.c:1596
- sys/dev/drm/amd/display/dc/core/dc_link_dp.c:1600
- sys/dev/drm/amd/display/dc/dc_dp_types.h:102
- sys/dev/drm/amd/display/dc/dc_dp_types.h:341
- sys/dev/drm/amd/display/dc/core/dc_link_dp.c:1978
Detail
Exploit chain
On real hardware: malicious DP sink/dongle raises short HPD with DEVICE_SERVICE_IRQ.AUTOMATED_TEST and DP_TEST_REQUEST = LINK_TRAINING|PHY_TEST_PATTERN; phase 1 poisons verified/cur_link_settings.lane_count=0xFF (and fires DF-1319's transient overflow), phase 2 dp_test_send_phy_test_pattern smashes ~3KB of its stack frame with 0..3-valued words -> non-canonical return address -> kernel panic (DoS). Subsequent HPD RX IRQs also walk 122 bytes past the 6-byte hpd_irq_data local. No path from these constrained values to controlled RIP.
Evidence (decisive lines)
run.log (model output: OOB WRITE 12..3012 bytes / OOB READ 10..122 bytes per sink-fed byte class, TRIGGER_RC=0), env.txt (NO_AMD_GPU, amdgpu.ko unloaded, guest-src md5 == audited), VERDICT.md (path:line trace), trigger.c (annotated hardware precondition), fix.diff (source validation + two loop caps, git-apply clean)
PoC changes
No seed PoC. Authored trigger.c as an annotated hardware-precondition + userland arithmetic model of the two new sinks (a syscall-level PoC is meaningless for a DP-sink bug); honest hardware-absent verification per family convention (DF-1319/DF-3759/DF-3604 pattern).
Verified recommended fix
Validate DP_TEST_LANE_COUNT in {1,2,4} and DP_TEST_LINK_RATE against the known enum in dp_test_send_link_training; cap the two cur_link_settings loops at LANE_COUNT_DP_MAX (see fix.diff).
Verdict
Source-level certain, runtime hardware-gated (inconclusive per family convention). Pass-2 depth on sys/dev/drm/amd/display/dc/core/dc_link_dp.c beyond known DF-1319 (whose scope is the transient lt_settings overflow inside the link-training helpers): dp_test_send_link_training (dc_link_dp.c:1772-1785) not only feeds the LT helpers, it PERSISTS the raw sink byte DP_TEST_LANE_COUNT into verified_link_cap (:1784) and, via dp_retrain_link_dp_test (dc_link_hwss.c:319 'link->cur_link_settings = link_setting'), into cur_link_settings - where it OUTLIVES training. Two sinks DF-1319 never reaches: (A) dp_test_send_phy_test_pattern :1881-1895 writes link_settings.lane_settings[lane] for lane < cur_link_settings.lane_count into struct dc_link_training_settings.lane_settings[LANE_COUNT_DP_MAX=4] (dc_dp_types.h:102-105, 48 bytes) - a 0xFF byte yields a 25512=3060-byte stack write, ~3012 bytes past the array (aux OOB read :1885 dpcd_lane_adjustment[lane/2] up to 126 bytes past a 2-element array feeds the values); handle_automated_test :1978-1995 executes the LINK_TRAINING and PHY_TEST_PATTERN phases in the SAME HPD RX IRQ, so one malicious sink IRQ reaches (A) directly; (B) hpd_rx_irq_check_link_loss_status :1596-1615 get_nibble_at_index(&hpd_irq_data.bytes.lane01_status.raw, lane) reads buf[lane/2] up to index 127 vs the 6-byte union hpd_irq_data (dc_dp_types.h:341-351) -> 122-byte stack OOB read in dc_link_handle_hpd_rx_irq's frame on the NEXT HPD RX IRQ. Written values are constrained to 0..3 per 4-byte enum word, so the primitive is a deterministic panic (non-canonical smashed return addresses), not controlled RIP. Runtime: guest has bochs VGA only, zero 0x1002 PCI devices, amdgpu.ko not loaded (env.txt) -> chain unexecutable; trigger.c (arithmetic model of both sinks) builds and runs on the guest, run.log TRIGGER_RC=0 with extents for byte classes {5,0x1f,0x20,0x7f,0xff}. Guest src md5 028326495b845611c38ec70dcd655bf3 == audited tree.
No comments yet.