/*
 * 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;
}
