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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3987 Β· 9 files
FileTypeDescriptionSize
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
README.md readme build/run/expected
↓ download 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.

VERDICT.md verdict full path:line trace
↓ download raw

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)

  1. dp_test_send_link_training β€” dc_link_dp.c:1772-1781 reads raw sink bytes DP_TEST_LANE_COUNT / DP_TEST_LINK_RATE (1 byte each, no validation against LANE_COUNT_DP_MAX=4 β€” dc_dp_types.h:31-37).
  2. :1784-1785 persists the byte into link->verified_link_cap.{lane_count, link_rate}.
  3. :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 executes link->cur_link_settings = *link_setting; β€” the raw byte now LIVES in cur_link_settings and survives until the next full mode set.
  4. handle_automated_test (dc_link_dp.c:1965-2010) processes DP_TEST_REQUEST bits 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 poisons cur_link_settings and immediately feeds it to dp_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.

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 -lv shows zero 0x1002 (AMD) devices (bochs VGA only), amdgpu.ko exists 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_testable
baseline no→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-3987

Confirmed kernel references

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.