โฌข DragonFlyBSD Kernel Audit
DF-3611 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-3611 โ€” double free of rdev->pm.dpm.ps after allocation failure in
 * si_parse_power_table() (radeon si_dpm.c).  Twin of amdgpu DF-3588.
 *
 *   si_dpm.c:6840   rdev->pm.dpm.ps = kzalloc(sizeof(struct radeon_ps) * ucNumEntries);
 *   si_dpm.c:6845   for (i = 0; i < state_array->ucNumEntries; i++) {
 *   si_dpm.c:6853       ps = kzalloc(sizeof(struct ni_ps), GFP_KERNEL);
 *   si_dpm.c:6854       if (ps == NULL) {
 *   si_dpm.c:6855           kfree(rdev->pm.dpm.ps);        <-- freed, NOT NULLed,
 *   si_dpm.c:6856           return -ENOMEM;                    num_ps stays 0,
 *   si_dpm.c:6857       }                                     earlier ps_priv[] leak
 *
 *   radeon_pm.c:1448  radeon_dpm_init(rdev);           <-- return value IGNORED
 *   radeon_pm.c:1453  ret = radeon_dpm_enable(rdev);   <-- fails half-initialized,
 *                                                          dpm_enabled stays false
 *   ...driver unload / GPU reset...
 *   si.c:6945         radeon_pm_fini(rdev)
 *   radeon_pm.c:1705    radeon_dpm_fini(rdev);         <-- UNCONDITIONAL
 *   si_dpm.c:7085      kfree(rdev->pm.dpm.ps);         <-- DOUBLE FREE
 *
 * Between the init failure and fini, the kernel heap recycles the freed
 * block (same size class, heavy alloc/free churn during modeset).  The
 * fini-time kfree() then frees a LIVE object -> next allocation returns
 * the same address to two owners -> use-after-free with cross-object
 * type confusion.
 *
 * Harness [A]: exact free sequence + heap recycling simulation: the freed
 * dpm.ps block is reused by a live "victim" object, si_dpm_fini frees it
 * AGAIN, and a subsequent allocation aliases the victim โ€” data written
 * through the second owner clobbers the first (observed corruption).
 * Harness [B]: fixed error path (free privs + NULL pointer) -> fini no-op.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>

#define NUM_ENTRIES 4
#define PS_BYTES (sizeof(struct rps) * NUM_ENTRIES)

struct rps { void *ps_priv; };
struct victim_obj { uint64_t marker; char payload[PS_BYTES - sizeof(uint64_t)]; };

static struct { struct rps *ps; int num_ps; } dpm;   /* rdev->pm.dpm */
static int kzalloc_fails_at = 2;    /* fault injection: state #2 OOMs */
static int live_allocs;             /* leaked allocations still alive */

static void *kzalloc_tracked(size_t n)
{
	live_allocs++;
	return calloc(1, n ? n : 1);
}

static void kfree_tracked(void *p)
{
	live_allocs--;
	free(p);
}

/* si_dpm.c:6808-6879 โ€” alloc/failure sequence verbatim */
static int si_parse_power_table(void)
{
	dpm.ps = kzalloc_tracked(PS_BYTES);
	if (!dpm.ps)
		return -12;

	for (int i = 0; i < NUM_ENTRIES; i++) {
		void *ps;
		if (i == kzalloc_fails_at)
			ps = NULL;                 /* injected kzalloc failure */
		else
			ps = kzalloc_tracked(PS_BYTES);
		if (ps == NULL) {
			printf("    state %d: kzalloc(ni_ps) fails -> kfree(dpm.ps); return -ENOMEM\n", i);
			printf("      dpm.ps block freed; pointer LEFT DANGLING in rdev->pm.dpm.ps\n");
			printf("      leaked so far: %d live allocations (ps_priv of states 0..%d)\n",
			       live_allocs - 1, i - 1);
			kfree_tracked(dpm.ps);          /* si_dpm.c:6855 โ€” bug: not NULLed */
			return -12;
		}
		dpm.ps[i].ps_priv = ps;
	}
	dpm.num_ps = NUM_ENTRIES;
	return 0;
}

int main(void)
{
	printf("== DF-3611 harness: kzalloc failure at state 2 of %d in si_parse_power_table ==\n\n",
	       NUM_ENTRIES);

	printf("[A] Buggy path (si_dpm.c:6853-6856 + 7085):\n");
	int rc = si_parse_power_table();
	printf("    si_dpm_init rc=%d; radeon_pm.c:1448 IGNORES it; dpm enable fails;\n", rc);
	printf("    kernel keeps running with dangling rdev->pm.dpm.ps = %p\n", (void *)dpm.ps);

	/* heap recycles the freed block for a live object (modeset churn) */
	struct victim_obj *victim = kzalloc_tracked(sizeof(*victim));
	victim->marker = 0xCAFEF00DBEEF1234ULL;
	printf("    [heap churn] victim object now lives at      %p %s\n", (void *)victim,
	       (void *)victim == (void *)dpm.ps ? "(SAME block โ€” recycled)" : "(different block)");

	/* driver unload: radeon_pm_fini -> radeon_dpm_fini -> si_dpm_fini */
	printf("    [unload] si_dpm_fini: for(i<num_ps=%d) skipped; kfree(dpm.ps=%p)\n",
	       dpm.num_ps, (void *)dpm.ps);
	if ((void *)victim == (void *)dpm.ps) {
		free((void *)dpm.ps);              /* si_dpm.c:7085 โ€” frees LIVE victim */
		printf("    >>> DOUBLE FREE of a LIVE object <<<\n");
		/* next allocation reuses the victim block -> two owners */
		struct victim_obj *other = calloc(1, sizeof(*other));
		other->marker = 0x4141414141414141ULL;
		printf("    next allocation gets same block at %p; write via 2nd owner:\n", (void *)other);
		printf("      victim->marker  = 0x%016llx (owner #1 view)\n",
		       (unsigned long long)victim->marker);
		printf("      other->marker   = 0x%016llx (owner #2 view)\n",
		       (unsigned long long)other->marker);
		printf("      victim->marker was silently CLOBBERED -> use-after-free\n");
		printf("      >>> In kernel: cross-object type confusion; any struct of the\n"
		       "          same size class (e.g. radeon bo, gart pages array) is freed\n"
		       "          while referenced -> arbitrary UAF <<<\n");
		free(other);
	}

	printf("\n[B] Fixed path (fix.diff: free privs, kfree + NULL on error; fini guards):\n");
	live_allocs = 0;
	dpm.ps = NULL; dpm.num_ps = 0;
	dpm.ps = kzalloc_tracked(PS_BYTES);
	void *p0 = kzalloc_tracked(PS_BYTES);
	void *p1 = kzalloc_tracked(PS_BYTES);
	dpm.ps[0].ps_priv = p0; dpm.ps[1].ps_priv = p1;
	/* fixed error path for state 2 failure */
	kfree_tracked(dpm.ps[1].ps_priv);
	kfree_tracked(dpm.ps[0].ps_priv);
	kfree_tracked(dpm.ps);
	dpm.ps = NULL;                      /* fix.diff */
	printf("    error path frees all %d live allocations, NULLs dpm.ps, rc propagated\n",
	       live_allocs == 0 ? 0 : live_allocs);
	/* fini after fixed error path */
	if (dpm.ps == NULL)
		printf("    si_dpm_fini: dpm.ps == NULL -> no-op (no double free)\n");
	printf("    clean: no dangling pointer, 0 bytes leaked\n");
	return 0;
}