โฌข DragonFlyBSD Kernel Audit
DF-3728 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-3728 harness โ€” ciss_notify_complete() fatal-error path
 * (sys/dev/raid/ciss/ciss.c:3619-3627) releases the notify request back to
 * the free queue WITHOUT clearing sc->ciss_periodic_notify (unlike the two
 * other exit paths at ciss.c:3648 and 3653). The dangling pointer is then
 * reused by the next ciss_periodic() tick via ciss_notify_event()
 * (ciss.c:3526): the SAME ciss_request is handed out to a new submitter
 * (CAM I/O or a BMIC command) AND re-used/re-posted by the notify chain,
 * giving double-ownership, command clobbering (ciss_preen_command +
 * notify CDB overwrite) and a double completion โ€” the CAM ccb gets
 * xpt_done()'d twice (use-after-free in CAM).
 *
 * This userspace replica models the request/free-list state machine exactly
 * and drives the fatal-error sequence. Trigger precondition in the kernel:
 * the adapter must return a fatal command status (!= SUCCESS,
 * TARGET_STATUS, TIMEOUT โ€” e.g. CISS_CMD_STATUS_PROTOCOL_ERROR or
 * HARDWARE_ERROR) for the Notify-on-Event command (flaky/distressed
 * firmware), then 10s later ciss_periodic restarts the chain.
 *
 * Build: cc -O2 -Wall -o harness harness.c
 * Fixed: cc -DFIXED ... adds `sc->ciss_periodic_notify = NULL;` to the
 *        fatal path (fix.diff).
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* ---- statuses (cissreg.h:122-134) ---- */
#define CISS_CMD_STATUS_SUCCESS			0
#define CISS_CMD_STATUS_TARGET_STATUS		1
#define CISS_CMD_STATUS_PROTOCOL_ERROR		5
#define CISS_CMD_STATUS_HARDWARE_ERROR		6
#define CISS_CMD_STATUS_TIMEOUT			11

#define CISS_FLAG_NOTIFY_OK	(1<<0)
#define CISS_MAX_REQUESTS		16

struct ciss_request_rep {
	int		 cr_tag;
	int		 cr_on_freeq;
	int		 cr_busy;		/* CISS_REQ_BUSY */
	int		 cr_posts;		/* times posted to adapter */
	int		 cr_completions;	/* times adapter completed it */
	void		*cr_data;		/* owner's data buffer */
	const char	*cr_owner;		/* who owns it now */
};

static struct {
	struct ciss_request_rep req[CISS_MAX_REQUESTS];
	struct ciss_request_rep *freeq[CISS_MAX_REQUESTS];
	int nfree;
	struct ciss_request_rep *periodic_notify;
	int flags;
	int cam_callbacks;		/* xpt_done() invocations */
	void *cam_ccb;
} sc;

static void notify_complete_rep(struct ciss_request_rep *cr);

static struct ciss_request_rep *
get_request(void)				/* ciss_get_request, LIFO freeq */
{
	if (sc.nfree == 0)
		return NULL;
	return sc.freeq[--sc.nfree];
}

static void
release_request(struct ciss_request_rep *cr)	/* ciss_release_request */
{
	sc.freeq[sc.nfree++] = cr;
	cr->cr_on_freeq = 1;
	cr->cr_owner = "freeq";
}

static void
post(struct ciss_request_rep *cr)		/* ciss_start -> adapter IPQ */
{
	cr->cr_posts++;
	cr->cr_busy = 1;
}

static void
adapter_complete(struct ciss_request_rep *cr)	/* ciss_complete dispatch */
{
	cr->cr_completions++;
	cr->cr_busy = 0;
	/* in the kernel the callback of the CURRENT owner runs here */
	if (strcmp(cr->cr_owner, "CAM") == 0)
		sc.cam_callbacks++;		/* ciss_cam_complete -> xpt_done(ccb) */
	else if (strcmp(cr->cr_owner, "notify") == 0)
		notify_complete_rep(cr);
}

static void
notify_event(void)				/* ciss_notify_event (ciss.c:3517) */
{
	struct ciss_request_rep *cr = sc.periodic_notify;	/* ciss.c:3526 */

	if (cr == NULL) {
		cr = get_request();				/* ciss.c:3530 */
		if (cr == NULL)
			return;
		sc.periodic_notify = cr;				/* 3534 */
		cr->cr_owner = "notify";
		cr->cr_data = (void *)0x51100000UL;	/* kmalloc(512) โ€” placeholder */
	}
	/* ciss.c:3544: if (cr->cr_data == NULL) alloc โ€” NOTE: for the stale
	 * pointer this reads the NEW owner's data pointer, skips alloc, and
	 * the notify CDB below CLOBBERS the new owner's command. */
	cr->cr_data = cr->cr_data ? cr->cr_data : (void *)0x51100000UL;
	post(cr);						/* ciss_start, 3579 */
	sc.flags |= CISS_FLAG_NOTIFY_OK;			/* 3593 */
}

static void
notify_complete_rep(struct ciss_request_rep *cr)	/* ciss_notify_complete */
{
	int command_status = CISS_CMD_STATUS_PROTOCOL_ERROR;	/* fatal */

	/* ciss.c:3619-3627 โ€” the fatal-error path */
	if ((command_status != CISS_CMD_STATUS_SUCCESS) &&
	    (command_status != CISS_CMD_STATUS_TARGET_STATUS) &&
	    (command_status != CISS_CMD_STATUS_TIMEOUT)) {
		printf("    notify_complete: fatal status %d -> release_request(cr)\n"
		       "    (ciss.c:3624 releases; ciss.c:3625 clears NOTIFY_OK;\n"
		       "     ciss_periodic_notify is NOT cleared โ€” dangling)\n",
		       command_status);
		release_request(cr);			/* ciss.c:3624 */
		sc.flags &= ~CISS_FLAG_NOTIFY_OK;	/* ciss.c:3625 */
#ifdef FIXED
		sc.periodic_notify = NULL;		/* the fix */
#endif
		return;
	}
}

int main(void)
{
	struct ciss_request_rep *cam_cr;
	int i, oob = 0;

	printf("== DF-3728 ciss_notify_complete dangling periodic_notify ==\n\n");

	/* populate free queue (tag order like ciss_init_requests) */
	for (i = CISS_MAX_REQUESTS - 1; i >= 1; i--) {
		sc.req[i].cr_tag = i;
		release_request(&sc.req[i]);
	}

	/* step 1: periodic starts the notify chain */
	notify_event();
	printf("step1: notify chain started, periodic_notify=tag%d, posts=%d\n\n",
	       sc.periodic_notify->cr_tag, sc.periodic_notify->cr_posts);

	/* step 2: adapter completes the notify command with a FATAL status */
	adapter_complete(sc.periodic_notify);
	printf("step2: fatal completion done; periodic_notify = %s\n",
	       sc.periodic_notify == NULL ? "NULL (cleared)" :
	       (sc.periodic_notify->cr_on_freeq ?
		"dangling (request back on freeq)" : "still owned"));
	if (sc.periodic_notify == NULL) {
		printf("       (with the fix the pointer is cleared โ€” no"
		       " dangling reuse)\n\n");
		return 0;
	}
	printf("\n");

	/* step 3: a CAM I/O grabs the SAME request off the free queue */
	cam_cr = get_request();
	if (cam_cr == NULL || cam_cr != sc.periodic_notify) {
		printf("step3: free queue returned a different request โ€” "
		       "scenario not exercised\n");
		return 1;
	}
	cam_cr->cr_owner = "CAM";
	cam_cr->cr_data = (void *)0x52200000UL;		/* csio->data_ptr */
	sc.cam_ccb = (void *)0x53300000UL;
	printf("step3: CAM I/O reuses tag%d (data=%p), posts=%d\n\n",
	       cam_cr->cr_tag, cam_cr->cr_data, cam_cr->cr_posts);
	post(cam_cr);

	/* step 4: 10s later ciss_periodic sees !NOTIFY_OK -> notify_event()
	 * follows the STALE periodic_notify (ciss.c:3526) and double-uses it */
	printf("step4: ciss_periodic -> ciss_notify_event uses stale pointer:\n");
	notify_event();
	if (cam_cr->cr_posts != 2 || sc.periodic_notify->cr_data == (void *)0x52200000UL)
		oob = 1;
	printf("       tag%d now posts=%d (DOUBLE POST), data=%p (CAM's buffer"
	       " kept: 0x%x), completions so far=%d\n\n",
	       cam_cr->cr_tag, cam_cr->cr_posts,
	       sc.periodic_notify->cr_data,
	       sc.periodic_notify->cr_data == (void *)0x52200000UL,
	       cam_cr->cr_completions);

	/* step 5: the adapter completes EACH post of the command once */
	for (i = 0; i < cam_cr->cr_posts; i++)
		adapter_complete(cam_cr);
	printf("step5: two completions delivered -> CAM callback (xpt_done) ran"
	       " %d time(s) for ONE ccb\n\n", sc.cam_callbacks);

	if (oob || sc.cam_callbacks >= 2) {
		printf("[BUG REPRODUCED] double-ownership of ciss_request tag%d:\n"
		       "  - re-posted while in flight by CAM (posts=%d)\n"
		       "  - CAM ccb completed twice (xpt_done x%d => CAM UAF)\n"
		       "  - notify CDB/preen clobbered the in-flight CAM command\n",
		       cam_cr->cr_tag, cam_cr->cr_posts, sc.cam_callbacks);
		return 0;
	}
#ifdef FIXED
	printf("[FIXED] fatal path clears periodic_notify: notify chain gets a\n"
	       "fresh request; CAM request untouched (posts=%d, callbacks=%d).\n",
	       cam_cr->cr_posts, sc.cam_callbacks);
	return 0;
#else
	printf("[NOT REPRODUCED]\n");
	return 1;
#endif
}