⬢ DragonFlyBSD Kernel Audit
DF-4479 / vinum_setstate_logic.c
← back to finding ↓ download raw
/*
 * DF-4479 PoC: vinum setstate() object_initialized state-machine logic bug.
 *
 * sys/dev/raid/vinum/vinumstate.c:1036-1055:
 *   set_sd_state(msg->index, sd_initialized, msg->force);
 *   if (sd->state != sd_initializing) {      <-- WRONG STATE COMPARED
 *       ioctl_reply->error = EBUSY;          the request was sd_initialized
 *   }
 *
 * After a successful transition to sd_initialized the check compares against
 * sd_initializing, so a *successful* VINUM_SETSTATE(object_initialized)
 * always reports "Can't set state" / EBUSY even though the kernel state
 * machine DID perform the transition (verified here by reading the sd state
 * back with VINUM_SDCONFIG).
 *
 * Also documented (dead-code variant of the same arithmetic confusion):
 * vinumstate.c:250-262 intends `sd->state = sd_initialized' as a terminal
 * "finished initializing" outcome, but status stays 1 and line 285-286
 * overwrites it with newstate (sd_up) -- reachable only through inconsistent
 * configs, see VERDICT.md.
 *
 * Needs: kldload vinum; root on /dev/vinum/control.
 */
#include <sys/types.h>
#include <sys/ioccom.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUFSIZE		1024
#define REPLYBUF	256

#define VINUM_CREATE	_IOC(IOC_INOUT, 'F', 64, BUFSIZE)
#define VINUM_SETSTATE	_IOC(IOC_INOUT, 'F', 76, REPLYBUF)

enum { drive_object = 0, sd_object = 1, plex_object = 2, volume_object = 3 };
enum { object_down = 0, object_initializing = 1, object_initialized = 2,
       object_up = 3 };

/* enum sdstate order (vinumvar.h / statetexts.h):
 * 0 unallocated, 1 uninit, 2 referenced, 3 init, 4 empty, 5 initializing,
 * 6 initialized, 7 obsolete, 8 stale, 9 crashed, 10 down, 11 reviving,
 * 12 reborn, 13 up */
static const char *sdstatename[] = {
	"sd_unallocated", "sd_uninit", "sd_referenced", "sd_init",
	"sd_empty", "sd_initializing", "sd_initialized", "sd_obsolete",
	"sd_stale", "sd_crashed", "sd_down", "sd_reviving",
	"sd_reborn", "sd_up"
};

struct vinum_ioctl_msg_poc {
	int index;
	int type;
	int state;
	int op;
	int force;
	int recurse;
	int verify;
	int otherobject;
	int rename;
	long long offset;
	int blocksize;
};

static int fd;

static int
send_line(const char *line)
{
	char buf[BUFSIZE];
	int rc;

	memset(buf, 0, sizeof(buf));
	snprintf(buf, sizeof(buf), "%s", line);
	rc = ioctl(fd, VINUM_CREATE, buf);
	if (rc || *(int *)buf)
		printf("create \"%s\": rc=%d errno=%d err=%d msg=\"%.60s\"\n",
		    line, rc, errno, *(int *)buf, buf + 4);
	return (rc);
}

static int
sd_state_readback(int index, int *stateout)
{
	/* VINUM_SDCONFIG = _IOWR('F', 67, struct sd): brute-force the size
	 * (kernel struct sd is 208 bytes on amd64) until the cmd matches. */
	static int cached_size = 0;
	unsigned char buf[4096];
	int size, rc, st;

	if (!cached_size) {
		for (size = 64; size <= 1024; size += 8) {
			unsigned long cmd = (unsigned long)
			    (0xC0000000u | ((unsigned)size << 16) |
			     ('F' << 8) | 67);

			memset(buf, 0, sizeof(buf));
			*(int *)buf = index;
			errno = 0;
			rc = ioctl(fd, cmd, buf);
			if (rc == 0) {
				cached_size = size;
				break;
			}
		}
		if (!cached_size) {
			printf("readback: could not find struct sd size\n");
			return (-1);
		}
		printf("readback: struct sd size = %d\n", cached_size);
	}
	memset(buf, 0, sizeof(buf));
	*(int *)buf = index;
	{
		unsigned long cmd = (unsigned long)
		    (0xC0000000u | ((unsigned)cached_size << 16) |
		     ('F' << 8) | 67);
		rc = ioctl(fd, cmd, buf);
	}
	if (rc != 0) {
		printf("readback ioctl rc=%d errno=%d\n", rc, errno);
		return (-1);
	}
	/* enum sdstate state sits at offset 64 (after name[MAXSDNAME=64]) */
	st = *(int *)(buf + 64);
	*stateout = st;
	return (0);
}

static void
do_setstate(int index, int type, int state, int force, const char *what)
{
	char buf[REPLYBUF];
	struct vinum_ioctl_msg_poc *msg = (struct vinum_ioctl_msg_poc *)buf;
	int st = -1;

	memset(buf, 0, sizeof(buf));
	msg->index = index;
	msg->type = type;
	msg->state = state;
	msg->force = force;
	ioctl(fd, VINUM_SETSTATE, buf);
	sd_state_readback(0, &st);
	printf("%-46s -> reply.error=%d msg=\"%.24s\" actual sd0 state=%d(%s)\n",
	    what, *(int *)buf, buf + 4, st,
	    (st >= 0 && st <= 13) ? sdstatename[st] : "<?>");
}

int
main(void)
{
	int st = -1;

	fd = open("/dev/vinum/control", O_RDWR);
	if (fd < 0) {
		perror("open /dev/vinum/control (need root, kldload vinum)");
		return (1);
	}

	/* build one sd on a referenced (never-opened) drive; driveoffset
	 * keeps set_sd_state() on the allocated-space path */
	send_line("drive d0");
	send_line("sd name s0 drive d0 len 2048s driveoffset 0s");
	sd_state_readback(0, &st);
	printf("after create: sd0 state=%s\n",
	    (st >= 0 && st <= 13) ? sdstatename[st] : "<?>");

	/* 1. force sd into sd_initializing */
	do_setstate(0, sd_object, object_initializing, 1,
	    "SETSTATE(sd0, object_initializing, force)");

	/* 2. THE BUG: request object_initialized with force */
	do_setstate(0, sd_object, object_initialized, 1,
	    "SETSTATE(sd0, object_initialized, force)");

	/* 3. idempotence control: same request again */
	do_setstate(0, sd_object, object_initialized, 1,
	    "SETSTATE(sd0, object_initialized, force) again");

	printf("\nBUG CRITERION: in step 2 the sd transitions to\n"
	    "sd_initialized (actual state above) but the ioctl reports\n"
	    "EBUSY \"Can't set state\" because vinumstate.c:1046 compares\n"
	    "sd->state != sd_initializing after requesting sd_initialized.\n");
	return (0);
}