⬢ DragonFlyBSD Kernel Audit
DF-4237 / sili_ncq_pool_model.c
← back to finding ↓ download raw
/*
 * sili_ncq_pool_model.c - DF-4237 evidence model
 *
 * Userspace model of the DragonFly sili(4) CAM ccb-pool accounting that
 * proves sili_ata_get_xfer() can return NULL while sili_cam.c dereferences
 * the result without a check:
 *
 *   - sili_cam.c:289-291  IDENTIFY probe:  xa = sili_ata_get_xfer(ap, atx);
 *                          xa->complete = ...;                 <- NULL deref
 *   - sili_cam.c:371-372  NCQ renegotiation loop:
 *                          xa = sili_ata_get_xfer(ap, atx);
 *                          if (xa->tag < at->at_ncqdepth)      <- NULL deref
 *
 * Kernel facts encoded (see VERDICT.md for path:line citations):
 *   - sc_ncmds     = SILI_MAX_CMDS = 31 slots, tags 0..30   (sili.h:706)
 *   - slot 1 is ap_err_ccb, never on the free list           (sili.c:265-268)
 *   - free TAILQ initialised in tag order: [0,2,3,...,30]    (sili.c:268)
 *   - sili_get_ccb:  pop head, returns NULL when empty       (sili.c:2003-2018)
 *   - sili_put_ccb:  push tail                              (sili.c:2021-2030)
 *   - CAM devq opening = sc_ncmds - 1 = 30                   (sili_cam.c:101-104)
 *   - NCQ renegotiation loop runs sc_ncmds iterations and
 *     permanently HOLDS every slot whose tag >= at_ncqdepth  (sili_cam.c:370-376)
 *   - at_ncqdepth = (device qdepth & 0x1F) + 1, clamped to sc_ncmds
 *                                                           (sili_cam.c:365-368)
 *   - a softreset success sets ap_probe = NEED_IDENT *without* revoking
 *     in-flight ccbs                                        (sili.c:845-849)
 *   - XPT_PATH_INQ(wildcard)/XPT_ENG_EXEC/port-thread probes run with no
 *     devq credit                                            (sili_cam.c:846-850,878-881; sili.c:511-512)
 *
 * Scenarios:
 *   A: XPT_SCSI_IO-triggered reprobe, 29 in-flight, device re-reports
 *      qdepth=1 (ncqdepth=2)  -> loop drains pool -> NULL deref at :371-372
 *   B: port-thread / PATH_INQ probe, 30 in-flight -> IDENTIFY get_xfer NULL
 *      -> deref at :289-290
 *   C: control - boot-time probe, no in-flight I/O -> no NULL (bug needs
 *      the concurrency precondition).
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SILI_MAX_CMDS	31			/* sili.h:706 */
#define DEVQ_OPENING	(SILI_MAX_CMDS - 1)	/* sili_cam.c:101-104 */

struct pool {
	int	free[SILI_MAX_CMDS];		/* TAILQ emulation: head=idx 0 */
	int	nfree;
	int	inflight[SILI_MAX_CMDS];	/* tag -> held by CAM I/O */
	int	held[SILI_MAX_CMDS];		/* tag -> held by NCQ loop */
};

static struct pool P;

static void
pool_init(void)
{
	int i;
	memset(&P, 0, sizeof(P));
	/* sili.c:265-268: slot 1 reserved as ap_err_ccb, others INSERT_TAIL
	 * in ascending tag order */
	for (i = 0; i < SILI_MAX_CMDS; ++i) {
		if (i == 1)
			continue;
		P.free[P.nfree++] = i;
	}
}

/* sili_get_ccb (sili.c:2003-2018): pop head, NULL when empty */
static int
get_xfer(void)
{
	int tag, i;
	if (P.nfree == 0)
		return -1;			/* NULL */
	tag = P.free[0];
	for (i = 1; i < P.nfree; ++i)
		P.free[i-1] = P.free[i];
	P.nfree--;
	return tag;
}

/* sili_put_ccb (sili.c:2021-2030): push tail */
static void
put_xfer(int tag)
{
	P.free[P.nfree++] = tag;
}

static void
mark_inflight(int tag)
{
	P.inflight[tag] = 1;
}

/* count = how many of the first 'n' free-list slots CAM I/O grabbed */
static void
cam_io(int n)
{
	int i, tag;
	for (i = 0; i < n; ++i) {
		tag = get_xfer();
		if (tag < 0) {
			printf("  cam_io: get_xfer NULL (pool exhausted)\n");
			exit(1);
		}
		mark_inflight(tag);
	}
}

/*
 * sili_cam.c:289-321 - the IDENTIFY step of sili_cam_probe().
 * Returns the xa (tag) or emulates the kernel NULL deref.
 */
static int
probe_identify(int check_null)
{
	int tag;
	tag = get_xfer();
	if (tag < 0) {
		if (!check_null) {
			/* kernel behaviour: xa->complete = sili_ata_dummy_done
			 * on a NULL xa  ==> page fault on the direct map of
			 * NULL+offsetof(struct ata_xfer, complete) */
			printf("  PANIC: sili_cam.c:290 xa->complete written "
			       "through NULL xa (probe with exhausted pool)\n");
			exit(2);
		}
		printf("  (patched) probe deferred, no free ccb\n");
		return -1;
	}
	put_xfer(tag);		/* sili_cam.c:321 */
	return tag;
}

/*
 * sili_cam.c:365-381 - NCQ renegotiation loop.
 * device_qdepth: value a (crafted) device reports in identify word 75.
 * check_null: 0 = model shipped kernel (deref), 1 = model patched guard.
 */
static int
ncq_renegotiate(int device_qdepth, int check_null)
{
	int at_ncqdepth = (device_qdepth & 0x1F) + 1;	/* sili_cam.c:365 */
	int i, tag, held = 0;

	if (at_ncqdepth > SILI_MAX_CMDS)
		at_ncqdepth = SILI_MAX_CMDS;		/* sili_cam.c:367-368 */
	if (at_ncqdepth <= 1)
		return 0;				/* loop gated off */

	for (i = 0; i < SILI_MAX_CMDS; ++i) {		/* sili_cam.c:370 */
		tag = get_xfer();
		if (tag < 0) {
			if (!check_null) {
				/* kernel behaviour: xa->tag read through
				 * NULL xa at sili_cam.c:372 */
				printf("  PANIC: sili_cam.c:372 xa->tag read "
				       "through NULL xa after loop held %d "
				       "slots (iteration %d/%d)\n",
				       held, i + 1, SILI_MAX_CMDS);
				exit(3);
			}
			printf("  (patched) loop stopped at iteration %d, "
			       "pool exhausted (held=%d)\n", i + 1, held);
			return held;
		}
		if (tag < at_ncqdepth) {		/* sili_cam.c:372 */
			put_xfer(tag);			/* cycles forever */
		} else {
			P.held[tag] = 1;		/* held forever */
			held++;
		}
	}
	return held;
}

static int
nfree(void)
{
	return P.nfree;
}

int
main(int argc, char **argv)
{
	/*
	 * argv[1] == "guard": model the proposed fix (NULL checks), which
	 * turns the panics into deferrals.  Default: shipped kernel.
	 */
	int guard = (argc > 1 && strcmp(argv[1], "guard") == 0);
	int deref = guard; /* pass guard flag as check_null: 0=kernel derefs, 1=guarded */

	printf("DF-4237 model: sili(4) ccb pool, sc_ncmds=%d, devq opening=%d, "
	       "slot 1 reserved, mode=%s\n", SILI_MAX_CMDS, DEVQ_OPENING,
	       guard ? "patched (NULL-guarded)" : "shipped kernel");

	printf("\nScenario C (control): boot-time probe, no in-flight I/O\n");
	pool_init();
	printf("  free=%d [", nfree());
	probe_identify(deref);
	ncq_renegotiate(31 /*device reports 31*/, deref);
	printf("%d tags cycling, no NULL - probe succeeds on idle port]\n",
	       nfree());
	printf("  => bug NOT reachable without concurrent I/O (negative "
	       "control)\n");

	printf("\nScenario A: XPT_SCSI_IO reprobe (sili_cam.c:944-945), "
	       "29 in-flight, device re-reports qdepth=1\n");
	pool_init();
	cam_io(29);
	printf("  free=%d after 29 in-flight; old ncqdepth=31 so max_tags=30\n",
	       nfree());
	probe_identify(deref);		/* takes last free slot, puts it back */
	ncq_renegotiate(1, deref);	/* ncqdepth=2: only tag 0 cycles */
	if (guard)
		printf("  no panic in guarded mode\n");

	printf("\nScenario B: port-thread / XPT_PATH_INQ(wildcard) probe with "
	       "saturated queue (30 in-flight)\n");
	pool_init();
	cam_io(30);
	printf("  free=%d - probe caller holds no devq credit\n", nfree());
	probe_identify(deref);		/* NULL -> deref (or guarded defer) */
	if (guard)
		printf("  no panic in guarded mode\n");
	return 0;
}