⬢ DragonFlyBSD Kernel Audit
DF-3708 / harness.c
← back to finding ↓ download raw
/*
 * DF-3708 harness: arcmsr_action() frees an SRB that belongs to the
 * bus_dmamem_alloc() coherent pool with kfree() -- an invalid free.
 *
 * Kernel source replicated:
 *   sys/dev/raid/arcmsr/arcmsr.c:3013  srb = arcmsr_get_freesrb(acb)
 *   sys/dev/raid/arcmsr/arcmsr.c:3053  if ((flags & CAM_SG_LIST_PHYS) == 0
 *                                         || (flags & CAM_DATA_PHYS) != 0) {
 *   sys/dev/raid/arcmsr/arcmsr.c:3057      kfree(srb, M_DEVBUF);   <-- BUG
 *
 * The SRB pointers handed out by arcmsr_get_freesrb() are interior
 * pointers of the single coherent allocation made at attach time:
 *   arcmsr.c:4413  bus_dmamem_alloc(..., &acb->uncacheptr, ...)
 *   arcmsr.c:4231  acb->srbworkingQ[i] = acb->psrb_pool[i] = srb_tmp;
 *   arcmsr.c:4233  srb_tmp = (void *)((unsigned long)srb_tmp + SRB_SIZE);
 *
 * A userland model of the same allocator discipline (one big allocation,
 * hand out interior pointers at SRB_SIZE strides, then free() an interior
 * pointer exactly as the driver does) shows why the free is invalid:
 * the allocator (glibc here, kern_slaballoc.c in the kernel) never
 * received that pointer from a matching allocation.
 *
 * DFly kernel consequence (sys/kern/kern_slaballoc.c:_kfree):
 *   - kup > 0 path  (1430-1468): treats pool bytes as an oversized-allocation
 *     size, kmem_slab_free() on a pointer kmem never allocated -> kernel_map
 *     corruption;
 *   - zone path (1475-1478): KKASSERT(*kup < 0)/z_Magic checks panic on
 *     INVARIANTS kernels, or treat pool contents as SLZone metadata on
 *     production kernels -> slab freelist corruption.
 * Additionally the SRB is consumed from srbworkingQ and never returned
 * (arcmsr_free_srb is not called) -> queue slot leak.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

#define ARCMSR_MAX_FREESRB_NUM 384
/* SRB_SIZE = (sizeof(CCB)+0x1f) & ~0x1f ; struct is ~544+ bytes -> 576 */
#define SRB_SIZE 576

struct pool_hdr {
	void *pool;			/* acb->uncacheptr */
	void *slot[ARCMSR_MAX_FREESRB_NUM];
};

static struct pool_hdr P;
static int workingsrb_startindex;

static void map_free_srb(void)
{
	size_t i;
	P.pool = malloc((size_t)SRB_SIZE * ARCMSR_MAX_FREESRB_NUM);
	if (P.pool == NULL)
		exit(2);
	memset(P.pool, 0, (size_t)SRB_SIZE * ARCMSR_MAX_FREESRB_NUM);
	for (i = 0; i < ARCMSR_MAX_FREESRB_NUM; i++)
		P.slot[i] = (char *)P.pool + i * SRB_SIZE;	/* arcmsr.c:4231/4233 */
	workingsrb_startindex = 0;
}

/* model of arcmsr_get_freesrb (arcmsr.c:2581) */
static void *get_freesrb(void)
{
	return P.slot[workingsrb_startindex++ % ARCMSR_MAX_FREESRB_NUM];
}

/* CAM flag bits used by the vulnerable branch (arcmsr.c:3049-3058) */
#define CAM_SCATTER_VALID 0x00000010U
#define CAM_SG_LIST_PHYS  0x00000400U
#define CAM_DATA_PHYS     0x00000200U

static int arcmsr_action_sg_errorpath(unsigned int ccb_h_flags)
{
	void *srb = get_freesrb();		/* arcmsr.c:3013 */
	int aborted = 0;

	if ((ccb_h_flags & CAM_SG_LIST_PHYS) == 0 ||
	    (ccb_h_flags & CAM_DATA_PHYS) != 0) {
		/*
		 * arcmsr.c:3055-3058: xpt_done(pccb); kfree(srb, M_DEVBUF);
		 * The free below is the userland stand-in for kfree().
		 */
		printf("error-path taken: freeing srb=%p (pool=%p + %zu bytes)\n",
		    srb, P.pool, (size_t)((char *)srb - (char *)P.pool));
		fflush(stdout);
		free(srb);				/* == kfree(srb, M_DEVBUF) */
		aborted = 1;
	}
	return aborted;
}

int main(void)
{
	pid_t pid;
	int status;

	setvbuf(stdout, NULL, _IONBF, 0);	/* keep fork() buffers clean */
	map_free_srb();
	/*
	 * Consume a few SRBs first so the srb being freed is an INTERIOR
	 * pointer of the pool allocation (pool + n*SRB_SIZE), like every
	 * real kfree(srb) call in the driver error path.  (In the kernel
	 * even slot 0 is not a kmalloc pointer -- the pool comes from
	 * bus_dmamem_alloc(), not the slab allocator.)
	 */
	(void)get_freesrb();
	(void)get_freesrb();
	(void)get_freesrb();
	printf("pool base            = %p\n", P.pool);
	printf("SRB handed out       = %p (pool + 3*SRB_SIZE, interior pointer)\n",
	    P.slot[3]);
	printf("flag combo: CAM_SCATTER_VALID set, virtual SG list "
	    "(no CAM_SG_LIST_PHYS) -- legal CAM API input\n\n");

	/*
	 * Fork: the child performs the invalid free exactly as the driver
	 * does.  glibc aborts the child ("free(): invalid pointer/size"),
	 * the kernel analogue is the slab/INVARIANTS panic or silent slab
	 * corruption described above.
	 */
	pid = fork();
	if (pid == 0) {
		arcmsr_action_sg_errorpath(CAM_SCATTER_VALID);
		/* not reached on aborting allocators */
		printf("child: free() returned without abort "
		    "(allocator accepted interior pointer -- on DFly this is "
		    "the silent-corruption case)\n");
		_exit(0);
	}
	waitpid(pid, &status, 0);
	printf("child wait status: raw=0x%x WIFSIGNALED=%d WIFEXITED=%d\n",
	    status, WIFSIGNALED(status), WIFEXITED(status));
	if (WIFSIGNALED(status) ||
	    (WIFEXITED(status) && WEXITSTATUS(status) != 0)) {
		/*
		 * DFly's slab-based libc malloc detects the interior-pointer
		 * free and dies with:
		 *   assertion: z->z_Magic == ZALLOC_SLAB_MAGIC in _slabfree
		 * (exit(1) from libc's assertion handler on this system).
		 * The kernel's _kfree (kern_slaballoc.c:1478) makes the
		 * identical z_Magic check -- panic on INVARIANTS kernels,
		 * silent slab-metadata corruption on production kernels.
		 */
		printf("\n[+] DF-3708 CONFIRMED: allocator rejected the "
		    "interior-pointer free (child %s %d).\n"
		    "    The kernel kfree() of a bus_dmamem pool interior "
		    "pointer trips the same slab checks -> panic on "
		    "INVARIANTS, silent slab corruption otherwise.\n",
		    WIFSIGNALED(status) ? "killed by signal" :
		    "exited with status", WIFSIGNALED(status) ?
		    WTERMSIG(status) : WEXITSTATUS(status));
		return 0;
	}
	printf("\n[+] DF-3708 PARTIAL: allocator did not abort, but the freed "
	    "pointer is provably an interior pointer of the pool allocation "
	    "(pool=%p, freed offset=%zu)\n", P.pool, (size_t)3 * SRB_SIZE);
	return 1;
}