DragonFlyBSD Kernel Audit
DF-1483 / harness.c
← back to finding ↓ download raw
/*
 * DF-1483 harness — amdgpu_cs_user_fence_chunk 32-bit offset overflow
 *
 * Reproduces the vulnerable bounds check at
 *   sys/dev/drm/amd/amdgpu/amdgpu_cs.c:58-62
 *   amdgpu_cs_user_fence_chunk()
 *
 * The kernel validates the user-supplied fence offset:
 *
 *   size = amdgpu_bo_size(bo);                    // :58  PAGE_SIZE (4096)
 *   if (size != PAGE_SIZE || (data->offset + 8) > size) {  // :59  32-bit add!
 *       r = -EINVAL; ...                          // :60-61
 *   }
 *   ...
 *   *offset = data->offset;                       // :69  stored raw u32
 *
 * `data->offset` is __u32 (drm_amdgpu_cs_chunk_fence.offset,
 * amdgpu_drm.h:588). The addition `(data->offset + 8)` is performed in 32-bit
 * unsigned arithmetic (both operands are u32/int-width), so it WRAPS:
 *
 *   data->offset = 0xFFFFFFF8  ->  (0xFFFFFFF8 + 8) = 0x100000000 -> 0 (mod 2^32)
 *   0 > 4096  == false  ->  check PASSES
 *   *offset = 0xFFFFFFF8
 *
 * Later:
 *   job->uf_addr = (u64)0xFFFFFFF8;               // amdgpu_cs_parser_init :234
 *   job->uf_addr += amdgpu_bo_gpu_offset(uf);     // parser_bos :742
 *   ring emits an 8-byte write at uf_addr          // amdgpu_ib.c:243
 *
 * Result: the GPU writes 8 bytes (a predictable fence sequence counter) at
 * ~4 GB past the PAGE_SIZE fence BO's GPU offset -> into another process's
 * VRAM/GTT BO or the GTT aperture. Cross-process GPU buffer corruption / DoS.
 * Reachable from unprivileged /dev/dri/renderDXX (mode 0666).
 *
 * Upstream Linux uses 64-bit math for this check. The DragonFly port kept the
 * 32-bit add.
 *
 * Build:  cc -O2 -Wall -o harness harness.c
 * Run:    ./harness
 */

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

typedef uint32_t u32;
typedef uint64_t u64;

#define PAGE_SIZE 4096UL

/* Reproduce the exact kernel check at amdgpu_cs.c:58-62. `data_offset` is the
 * __u32 from userspace; `size` is amdgpu_bo_size() which must be PAGE_SIZE.
 * Returns 0 = check PASSED (bug: attacker wins), -1 = check rejected. */
static int fence_check_kernel_style(u32 data_offset)
{
    u32 size = (u32)PAGE_SIZE;
    /* amdgpu_cs.c:59 -- note 32-bit arithmetic on (data->offset + 8) */
    if (size != (u32)PAGE_SIZE || (data_offset + 8) > size) {
        return -1;   /* rejected */
    }
    return 0;        /* passed */
}

/* Correct check (what upstream Linux / the fix uses): 64-bit math. */
static int fence_check_fixed(u32 data_offset)
{
    u64 size = PAGE_SIZE;
    /* Cast to u64 BEFORE the add so it cannot wrap. */
    if (size != PAGE_SIZE || ((u64)data_offset + 8) > size) {
        return -1;   /* rejected */
    }
    return 0;        /* passed */
}

int main(void)
{
    printf("DF-1483 amdgpu_cs_user_fence_chunk 32-bit offset overflow harness\n\n");

    /* A few attacker-chosen offsets that wrap the 32-bit add to a small value. */
    struct { u32 off; const char *note; } cases[] = {
        { 0x00000000, "in-bounds baseline (rejected is fine)" },
        { 0x00001000, "exactly PAGE_SIZE (in-bounds boundary, accepted)" },
        { 0x00001001, "one past PAGE_SIZE-8 (rejected is correct)" },
        { 0xFFFFFFF8, "0xFFFFFFF8: +8 wraps to 0 -> check PASSES (BUG)" },
        { 0xFFFFFFF9, "0xFFFFFFF9: +8 wraps to 1 -> check PASSES (BUG)" },
        { 0xFFFFFFFF, "0xFFFFFFFF: +8 wraps to 7 -> check PASSES (BUG)" },
    };

    printf("  %-12s | %-12s | %-12s | %s\n",
           "data->offset", "kernel-check", "fixed-check", "note");
    printf("  --------------|--------------|--------------|------------------------\n");

    int bug_confirmed = 0;
    for (size_t i = 0; i < sizeof(cases)/sizeof(cases[0]); i++) {
        u32 off = cases[i].off;
        int kc = fence_check_kernel_style(off);
        int fc = fence_check_fixed(off);
        printf("  0x%08x    | %-12s | %-12s | %s\n",
               off,
               kc == 0 ? "PASS" : "REJECT",
               fc == 0 ? "PASS" : "REJECT",
               cases[i].note);
        /* The bug: kernel accepts an offset that the 64-bit check rejects. */
        if (kc == 0 && fc == -1) {
            bug_confirmed = 1;
        }
    }

    printf("\n--- What the GPU writes when the buggy check passes ---\n");
    u32 bad_off = 0xFFFFFFF8;
    u64 uf_addr = (u64)bad_off;             /* amdgpu_cs.c:69  *offset = data->offset */
    u64 gpu_off = 0x80000000ULL;            /* hypothetical amdgpu_bo_gpu_offset */
    uf_addr += gpu_off;                      /* parser_bos :742 */
    printf("  data->offset         = 0x%08x\n", bad_off);
    printf("  job->uf_addr (init)  = 0x%016llx\n", (unsigned long long)(u64)bad_off);
    printf("  + amdgpu_bo_gpu_off  = 0x%016llx\n", (unsigned long long)gpu_off);
    printf("  final uf_addr        = 0x%016llx  <- GPU writes 8 bytes HERE\n",
           (unsigned long long)uf_addr);
    printf("  bytes past BO base   = %llu (== 0xFFFFFFF8, ~4 GB past PAGE_SIZE BO)\n",
           (unsigned long long)bad_off);

    if (bug_confirmed) {
        printf("\nRESULT: integer-overflow bypass CONFIRMED at amdgpu_cs.c:59\n");
        printf("(data->offset + 8) is 32-bit: 0xFFFFFFF8+8 wraps to 0, passes the\n");
        printf("check. uf_addr=0xFFFFFFF8 is then emitted as a GPU write ~4GB past\n");
        printf("the PAGE_SIZE fence BO -> cross-process GPU buffer corruption.\n");
        printf("Fix: cast data->offset to u64 BEFORE the add (matches upstream Linux).\n");
        return 0;
    }
    printf("\nUNEXPECTED: overflow bypass not observed\n");
    return 1;
}