โฌข DragonFlyBSD Kernel Audit
DF-4820 / ring_restore_leak.c
โ† back to finding โ†“ download raw
/*
 * DF-4820 โ€” mock environment for the verbatim radeon_ring_restore() body.
 *
 * Reproduces, in userspace, the exact kernel conditions that hold when
 * radeon_ring_restore() is called from radeon_gpu_reset()
 * (sys/dev/drm/radeon/radeon_device.c:1879-1887):
 *
 *   - radeon_ring_lock() -> radeon_ring_alloc() fails with an error.
 *     Two deterministic failure modes exist at that point, both proven in
 *     the surrounding kernel source:
 *
 *     (1) -EDEADLK: the GPU was just reset, so rdev->needs_reset is still
 *         true (it is only cleared at radeon_device.c:1921-1922, AFTER the
 *         restore loop).  radeon_fence_wait_seq_timeout()
 *         (radeon_fence.c:511-517) has `|| rdev->needs_reset` in its wait
 *         condition and converts it to -EDEADLK.
 *
 *     (2) -ENOENT: no fences pending (radeon_fence.c:649-654).
 *
 *     alloc() can only reach the fence wait when the aligned request equals
 *     ring_size/4 dwords โ€” i.e. the ring was saturated (within align_mask
 *     dwords of full) when the GPU hung.  On the fresh post-reset ring
 *     (ring->wptr = 0, cik.c:4104) free_dw maxes at ring_size/4, so the
 *     request can never be satisfied -> error return is guaranteed.
 *
 *   - `data` points to a kvmalloc_array() allocation made by
 *     radeon_ring_backup() (radeon_ring.c:317).  The caller
 *     (radeon_device.c:1881-1882) ignores radeon_ring_restore()'s return
 *     value and never frees `data` on the success-of-reset branch.
 *
 * kvmalloc_array()/kvfree() are instrumented to track live allocations.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>

/* ---- kernel types / return codes (minimal mock) ---------------------- */
/* values are cosmetic for the mock; only sign matters to the body */
#define ENOENT 2
#define EDEADLK 35
#define ENOMEM 12

struct radeon_device { int dummy; };
struct radeon_ring {
	unsigned ring_size;	/* bytes, power of 2 */
	unsigned align_mask;
	unsigned ptr_mask;
	unsigned ring_free_dw;
};

/* ---- allocation accounting (all test blocks are the same size) ------- */
static unsigned long live_allocs;
static unsigned long total_allocs;
static unsigned long block_bytes;

static void *kvmalloc_array(size_t n, size_t sz, unsigned gfp)
{
	void *p = malloc(n * sz);
	if (p) {
		live_allocs++;
		total_allocs++;
	}
	return p;
}

static void kvfree(void *p)
{
	if (p) {
		if (live_allocs == 0) {
			fprintf(stderr, "kvfree of untracked pointer %p!\n", p);
			exit(3);
		}
		live_allocs--;
		free(p);
	}
}

/* ---- mocked collaborators -------------------------------------------- */

/* error injected by radeon_ring_lock(); -EDEADLK (mode 1) or -ENOENT (2) */
static int lock_err = -EDEADLK;

static int radeon_ring_lock(struct radeon_device *rdev,
			    struct radeon_ring *ring, unsigned ndw)
{
	/*
	 * Model of radeon_ring_alloc() (radeon_ring.c:104-127) reduced to
	 * its decision on the fresh (just-resumed, wptr=0) ring:
	 *
	 *   ndw    = 16383 (saturated-ring backup size: full - 1 dw)
	 *   aligned = (16383 + 0xf) & ~0xf = 16384 == ring->ring_size/4
	 *   while (16384 > 16384 - 1):
	 *       radeon_ring_free_size -> free_dw = 16384 (maximum)
	 *       ndw < free_dw ? 16384 < 16384 ? no
	 *       radeon_fence_wait_next -> -EDEADLK / -ENOENT -> return r
	 */
	unsigned aligned = (ndw + ring->align_mask) & ~ring->align_mask;

	if (aligned == ring->ring_size / 4) {
		printf("  [alloc-model] ndw=%u aligned=%u == ring_size/4=%u:"
		       " unsatisfiable on fresh ring -> fence wait -> error %d\n",
		       ndw, aligned, ring->ring_size / 4, lock_err);
		return lock_err;
	}
	return 0;
}

static void radeon_ring_write(struct radeon_ring *ring, uint32_t v) { }

static void radeon_ring_unlock_commit(struct radeon_device *rdev,
				      struct radeon_ring *ring, bool hdp_flush)
{
	/* successful path: body then calls kvfree(data) itself */
}
int radeon_ring_restore(struct radeon_device *rdev, struct radeon_ring *ring,
			unsigned size, uint32_t *data)
{
	int i, r;

	if (!size || !data)
		return 0;

	/* restore the saved ring content */
	r = radeon_ring_lock(rdev, ring, size);
	if (r)
	{
		kvfree(data);
		return r;
	}

	for (i = 0; i < size; ++i) {
		radeon_ring_write(ring, data[i]);
	}

	radeon_ring_unlock_commit(rdev, ring, false);
	kvfree(data);
	return 0;
}
/*
 * DF-4820 main: drive the verbatim radeon_ring_restore() kernel body
 * (extracted mechanically from the kernel tree by build.sh) in the two
 * kernel-proven error modes, and measure leaked allocations.
 *
 * Each iteration simulates one GPU reset cycle:
 *   radeon_ring_backup()  -> kvmalloc_array(size)     [tracked]
 *   radeon_ring_restore() -> early `return r` w/o kvfree  [the bug]
 *   caller ignores retval (radeon_device.c:1881-1882)
 */
/* NOTE: no #include here โ€” build.sh concatenates harness_env.h + the
 * verbatim kernel body + this file into a single translation unit. */

int main(int argc, char **argv)
{
	struct radeon_device rdev = { 0 };
	struct radeon_ring ring = {
		.ring_size = 64 * 1024,		/* typical CP ring */
		.align_mask = 0xf,
		.ptr_mask = (64 * 1024 / 4) - 1,
		.ring_free_dw = 64 * 1024 / 4,
	};
	/* saturated-ring backup size: ring 100% full minus 1 dword
	 * (radeon_ring_alloc guarantees >= 1 spare dw; commit padding can
	 * bring free_dw to the align boundary โ€” 16383 is the max pending) */
	unsigned size = (ring.ring_size / 4) - 1;
	int iters = argc > 1 ? atoi(argv[1]) : 1000;
	int mode = argc > 2 ? atoi(argv[2]) : 1;	/* 1=-EDEADLK 2=-ENOENT */
	int i, r, failed = 0;

	block_bytes = (unsigned long)size * sizeof(uint32_t);
	lock_err = (mode == 2) ? -ENOENT : -EDEADLK;

	printf("DF-4820 radeon_ring_restore() backup-buffer leak harness\n");
	printf("ring_size=%u dw=%u align_mask=0x%x backup_size=%u dw (%lu B)\n",
	       ring.ring_size, ring.ring_size / 4, ring.align_mask,
	       size, block_bytes);
	printf("injected radeon_ring_lock error: %d (%s)\n", lock_err,
	       mode == 2 ? "-ENOENT (no pending fences)" :
			   "-EDEADLK (needs_reset still true during restore)");
	printf("iterations: %d%s\n\n", iters,
#ifdef FIXED
	       "  [FIXED build: kvfree(data) added on error path]"
#else
	       "  [BASELINE build: stock kernel body]"
#endif
	      );

	for (i = 0; i < iters; i++) {
		uint32_t *data = kvmalloc_array(size, sizeof(uint32_t), 0);
		if (!data) {
			fprintf(stderr, "oom at iter %d\n", i);
			return 2;
		}
		/* radeon_device.c:1881: return value ignored */
		r = radeon_ring_restore(&rdev, &ring, size, data);
		if (r)
			failed++;
		if (i == 0)
			printf("  iter 0: radeon_ring_restore -> %d\n", r);
	}

	printf("\ntotal restore calls : %d\n", iters);
	printf("returned error      : %d\n", failed);
	printf("allocations made    : %lu (%lu bytes)\n",
	       total_allocs, total_allocs * block_bytes);
	printf("allocations freed   : %lu\n", total_allocs - live_allocs);
	printf("LIVE (leaked) blocks: %lu  (%lu bytes = %lu KiB)\n",
	       live_allocs, live_allocs * block_bytes,
	       live_allocs * block_bytes / 1024);

#ifdef FIXED
	if (live_allocs != 0) {
		printf("RESULT: FIXED build still leaks โ€” FAILURE\n");
		return 1;
	}
	printf("RESULT: FIXED build leaks nothing โ€” fix VALIDATED\n");
#else
	if (live_allocs == (unsigned long)iters && failed == iters) {
		printf("RESULT: LEAK REPRODUCED โ€” every failed restore leaks "
		       "its %lu-byte kvmalloc backup buffer\n", block_bytes);
		return 0;
	}
	if (live_allocs == 0 && failed == 0) {
		printf("RESULT: no failure injected โ€” harness misconfigured\n");
		return 2;
	}
	printf("RESULT: unexpected state (live=%lu failed=%d)\n",
	       live_allocs, failed);
	return 2;
#endif
}