/*
 * iopl_lifecycle.c — IOPL grant lifecycle probe (privileged path).
 *
 * Demonstrates the accounting gap around
 *   sys/platform/pc64/x86_64/sysarch.c:52-65  cpu_set_iopl()/cpu_clr_iopl()
 *   sys/kern/kern_memio.c:184-218              /dev/io open/close
 *   sys/platform/pc64/x86_64/vm_machdep.c:159  fork trapframe bcopy
 *
 * Run as root. Port 0x80 (POST diag latch, benign in QEMU) is the probe.
 * A #GP on port access from CPL3 delivers SIGSEGV; caught with sigsetjmp.
 *
 * Chain:
 *  step 0  control: unprivileged-style port access -> SIGSEGV expected (IOPL 0)
 *  step 1  open /dev/io                -> IOPL becomes 3 in THIS lwp frame
 *  step 2  port OUT/IN succeeds
 *  step 3  fork(); child closes its inherited fd FIRST, parent closes
 *          LAST (mmclose -> cpu_clr_iopl on the parent = the closer);
 *          child never opened anything and holds no fd afterwards.
 *  step 4  child: IOPL must be 0 per io(4) contract ("while fd held") —
 *          observed: still 3, port I/O still works.
 *  step 5  child: setuid(65534) (nobody) — IOPL survives privilege drop,
 *          port I/O still works with zero fds and uid nobody.
 */
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

static sigjmp_buf gp_jb;
static volatile sig_atomic_t gp_hit;

static void gp_handler(int sig)
{
	gp_hit = 1;
	siglongjmp(gp_jb, 1);
}

static unsigned iopl_cur(void)
{
	unsigned long rf;
	__asm __volatile("pushfq; popq %0" : "=r"(rf));
	return (unsigned)((rf >> 12) & 3);
}

/* returns 0 = access ok, 1 = SIGSEGV (#GP), -1 = ??? */
static int try_port_io(void)
{
	unsigned char v;
	gp_hit = 0;
	if (sigsetjmp(gp_jb, 1) != 0)
		return gp_hit ? 1 : -1;
	__asm __volatile("outb %%al, %%dx" : : "a"((unsigned char)0x42), "d"((unsigned short)0x80));
	__asm __volatile("inb %%dx, %%al" : "=a"(v) : "d"((unsigned short)0x80));
	(void)v;
	return 0;
}

int main(void)
{
	int fd, r;
	pid_t pid;
	int status;

	signal(SIGSEGV, gp_handler);
	signal(SIGBUS, gp_handler);
	setvbuf(stdout, NULL, _IONBF, 0);

	printf("[step0] control port access, IOPL=%u -> ", iopl_cur());
	r = try_port_io();
	printf("%s (expect SIGSEGV/#GP)\n", r == 1 ? "SIGSEGV" : r == 0 ? "OK(!)" : "??");

	fd = open("/dev/io", O_RDONLY);
	if (fd < 0) {
		printf("open(/dev/io) failed: %s\n", strerror(errno));
		return 1;
	}
	printf("[step1] open(/dev/io) ok, IOPL now = %u (expect 3)\n", iopl_cur());
	printf("[step2] port access with fd: %s (expect OK)\n",
	       try_port_io() == 0 ? "OK" : "SIGSEGV(!)");

	fflush(stdout);
	pid = fork();
	if (pid == 0) {
		/* child: close inherited fd first so parent's close is the LAST */
		close(fd);
		sleep(2);	/* let parent do the last close */
		printf("[step4] child: all /dev/io fds closed, IOPL=%u (contract says 0)\n",
		       iopl_cur());
		printf("[step4] child: port access after last close: %s\n",
		       try_port_io() == 0 ? "OK — GRANT OUTLIVED THE FD" : "SIGSEGV");
		r = setuid(65534);
		if (r != 0) {
			printf("[step5] setuid(65534) failed: %s\n", strerror(errno));
			_exit(9);
		}
		printf("[step5] child: uid=%d IOPL=%u port access: %s\n",
		       getuid(), iopl_cur(),
		       try_port_io() == 0 ? "OK — GRANT SURVIVED PRIVILEGE DROP" : "SIGSEGV");
		_exit(0);
	}
	sleep(1);
	close(fd);	/* LAST close -> mmclose -> cpu_clr_iopl() on PARENT lwp */
	printf("[step3] parent closed fd (last close), parent IOPL=%u (expect 0 — clr hit closer)\n",
	       iopl_cur());
	waitpid(pid, &status, 0);
	printf("[done ] child exit status %d\n", WEXITSTATUS(status));
	return 0;
}
