DF-1297 / harness.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | /* * DF-1297 harness โ ci_thermal_setup_fan_table divide-by-zero * * Reproduces the vulnerable arithmetic of * sys/dev/drm/amd/powerplay/smumgr/ci_smumgr.c:2159-2166 * in userspace, feeding it a *crafted VBIOS* (ATOM_Tonga_Fan_Table) in which * usTMed == usTMin (so t_diff1 == 0) โ exactly what an attacker who controls * the GPU VBIOS (malicious ROM, faulty flash, VFIO passthrough of a hacked * card) can present. * * The kernel path is gated by: * - PHM_PlatformCaps_MicrocodeFanControl (set from VBIOS fan-table presence, * process_pptables_v1_0.c:933-934) * - fan present (bNoFan == 0, also VBIOS-derived) * - fan_table_start != 0 * - duty100 != 0 (hardware register read) * All of these are satisfied on any real CIK/Bonaire/Hawaii card with fan * control; the attacker only needs to also control the temperature fields in * the VBIOS image, which is the same trust boundary. * * Build: cc -O2 -o harness harness.c * Run: ./harness # prints "t_diff1=0 -> divide-by-zero CONFIRMED" * * NOTE: This is a userspace replica of the kernel arithmetic. The real kernel * path produces CPU trap 0 (#DE โ divide error) -> kernel panic. We trap the * SIGFPE in userspace to prove the arithmetic fault without needing the AMD * GPU / amdgpu module / CIK hardware on this guest. */ #include <stdio.h> #include <stdint.h> #include <stdlib.h> #include <signal.h> #include <setjmp.h> static sig_atomic_t got_fpe = 0; static jmp_buf jb; static void fpe_handler(int sig) { (void)sig; got_fpe = 1; longjmp(jb, 1); } /* Mirror of the relevant slice of ATOM_Tonga_Fan_Table (VBIOS, little-endian). */ struct vbios_fan_table { uint8_t ucTHyst; uint8_t pad; uint16_t usTMin; /* "temperature below which we run minimal PWM" */ uint16_t usTMed; /* "middle temperature where we change slopes" */ uint16_t usTHigh; /* "high temperature for second slope" */ uint16_t usPWMMin; uint16_t usPWMMed; uint16_t usPWMHigh; uint16_t usTMax; /* ... more fields ... */ }; /* Replica of ci_thermal_setup_fan_table's divisor block (ci_smumgr.c:2155-2166). * Returns 0 on success, -1 on arithmetic fault. * `volatile` + noinline mirrors the kernel: the VBIOS fields are only known at * runtime (read from GPU ROM), so the compiler cannot constant-fold the division * away. Without this, -O2 sees t_diff1==0 at compile time, declares the div UB, * and elides it โ which is NOT what the kernel does. */ __attribute__((noinline)) static int ci_slope_compute(volatile const struct vbios_fan_table *vb, uint32_t duty100, uint16_t *slope1_out, uint16_t *slope2_out) { uint32_t t_diff1, t_diff2, pwm_diff1, pwm_diff2; uint16_t slope1, slope2; /* ci_smumgr.c:2159-2160 -- deltas from VBIOS, NO validation */ t_diff1 = vb->usTMed - vb->usTMin; t_diff2 = vb->usTHigh - vb->usTMed; /* ci_smumgr.c:2162-2163 */ pwm_diff1 = vb->usPWMMed - vb->usPWMMin; pwm_diff2 = vb->usPWMHigh - vb->usPWMMed; /* ci_smumgr.c:2165-2166 -- divide by VBIOS deltas with NO zero check */ slope1 = (uint16_t)((50 + ((16 * duty100 * pwm_diff1) / t_diff1)) / 100); slope2 = (uint16_t)((50 + ((16 * duty100 * pwm_diff2) / t_diff2)) / 100); if (slope1_out) *slope1_out = slope1; if (slope2_out) *slope2_out = slope2; return 0; } int main(int argc, char **argv) { struct sigaction sa; sa.sa_handler = fpe_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGFPE, &sa, NULL); /* Crafted VBIOS: usTMed == usTMin -> t_diff1 == 0. * duty100 != 0 and pwm_diff1 != 0 ensure the division actually fires. * * We build the "VBIOS image" at runtime (touching the bytes through a * volatile sink) so the compiler cannot constant-fold the division away. * This mirrors the kernel: the VBIOS is parsed at runtime from GPU ROM, * so gcc emits a real `div` instruction and the CPU traps on #DE. (At -O2 * gcc treats integer div-by-zero as UB and elides it ONLY when it can see * the zero at compile time โ which the kernel never can.) */ volatile struct vbios_fan_table crafted; /* "Parse" the crafted VBIOS bytes (here, directly set the fields). */ crafted.ucTHyst = 5; crafted.usTMin = 2500; /* 25.00 C */ crafted.usTMed = 2500; /* == usTMin -> t_diff1 = 0 (the bug) */ crafted.usTHigh = 9000; crafted.usPWMMin = 100; crafted.usPWMMed = 200; /* pwm_diff1 = 100 != 0 */ crafted.usPWMHigh = 300; crafted.usTMax = 10900; /* argc-dependent sink so gcc cannot prune the whole thing as dead code. */ if (argc > 1 && argv[1][0] == 'q') return 0; uint32_t duty100 = (argc > 1) ? 100u : (100u + (uint32_t)(unsigned char)crafted.ucTHyst - 5); printf("DF-1297 ci_thermal_setup_fan_table divide-by-zero harness\n"); printf("VBIOS usTMin=%u usTMed=%u usTHigh=%u duty100=%u\n", crafted.usTMin, crafted.usTMed, crafted.usTHigh, duty100); printf("=> t_diff1 = usTMed-usTMin = %u (kernel divides by this)\n", crafted.usTMed - crafted.usTMin); if (setjmp(jb) == 0) { ci_slope_compute(&crafted, duty100, NULL, NULL); printf("UNEXPECTED: no arithmetic fault (t_diff1 was non-zero?)\n"); return 1; } /* If we got here, SIGFPE fired. */ printf("SIGFPE caught: integer divide-by-zero on t_diff1 == 0\n"); printf("In-kernel equivalent: CPU trap 0 (#DE) -> kernel panic\n"); printf("RESULT: divide-by-zero CONFIRMED at ci_smumgr.c:2165\n"); (void)got_fpe; return 0; } |