DF-4060 / fix.diff
# DF-4059 + DF-4060 -- sys/dev/raid/amr/amr.c # # DF-4059: amr_done() trusts the controller completion list twice over. # (a) completion id 0 yields idx = -1 and reads amr_busycmd[-1] -- the # last field of amr_ready (always &amr_ready.stqh_first, or # &requeued_cmd.ac_link after an EBUSY requeue) -- which is then # driven through amr_freeslot() as an amr_command*: with the queue # idle this lands on a NULL slot pointer and panics ("amr: slot 0 # not busy?"); with a requeued command it double-frees a live # command's slot; with slot 0 busy the forged ac_sc/ac_slot reads # steer a NULL store and an atomic decrement into arbitrary softc # fields and past the end of the softc (verified 1208 bytes past # the 3920-byte softc on the audit rig). id 0 is never allocated # (ident = slot + 1, amr.c:2038/2074), so reject it outright. # (b) on std firmware mb_nstatus is an 8-bit controller-chosen count # but mb_completed[] has 46 entries: the loop reads up to 255 # entries -- 209 bytes past the 82-byte stack mailbox -- and every # out-of-bounds byte is disclosed through the "bad slot %d # completed" print (verified: kernel heap pointers and caller # frame contents disclosed to the console log). Clamp the loop to # the array size. (The quartz-side WRITE overflow into # completed[46] inside amr_quartz_get_work is DF-1358 and is fixed # separately.) # # DF-4060: amr_std_poll_command() timeout leaves the slot permanently # busy (busycmd/busyslots/AMR_CMD_BUSY) while every caller # (amr_enquiry, amr_flush, amr_support_ext_cdb, amr_dump_blocks) # unconditionally amr_releasecmd()s the command. The next # amr_alloccmd/amr_start panics ("amr: slot %d busy?"), a late # completion completes the recycled command while it sits on the free # list (status store + completion callback into a free-listed command, # verified), and amr_rescan_drives spins forever on the poisoned # busyslots counter. Detach the slot on timeout so late completions --- a/sys/dev/raid/amr/amr.c 2026-10-15 11:53:50.510749327 +0000 +++ b/sys/dev/raid/amr/amr.c 2026-10-15 11:53:50.522749173 +0000 @@ -1356,8 +1356,21 @@ if (!(ac->ac_flags & AMR_CMD_BUSY)) { error = 0; } else { - /* XXX the slot is now marked permanently busy */ + /* + * Timed out. Detach the command from its slot so that the + * caller's amr_releasecmd() cannot recycle a command the + * controller may still complete late: a late completion will + * now be rejected as a bad slot instead of completing the + * recycled command. + */ error = EIO; + lockmgr(&sc->amr_hw_lock, LK_EXCLUSIVE); + if (sc->amr_busycmd[ac->ac_slot] == ac) { + sc->amr_busycmd[ac->ac_slot] = NULL; + atomic_subtract_int(&sc->amr_busyslots, 1); + } + lockmgr(&sc->amr_hw_lock, LK_RELEASE); + ac->ac_flags &= ~AMR_CMD_BUSY; device_printf(sc->amr_dev, "polled command timeout\n"); } return(error); @@ -1768,7 +1781,7 @@ ac_qhead_t head; struct amr_command *ac; struct amr_mailbox mbox; - int i, idx, result; + int i, idx, nstatus, result; debug_called(3); @@ -1781,9 +1794,27 @@ /* poll for a completed command's identifier and status */ if (sc->amr_get_work(sc, &mbox)) { result = 1; - + + /* + * The completion list has a fixed size. A larger count from + * the controller (mb_nstatus is 8 bits, the array holds 46) + * would read past the mailbox on our stack. + */ + nstatus = mbox.mb_nstatus; + if (nstatus > (int)nitems(mbox.mb_completed)) { + device_printf(sc->amr_dev, "excessive completion count %d\n", + nstatus); + nstatus = nitems(mbox.mb_completed); + } + /* iterate over completed commands in this result */ - for (i = 0; i < mbox.mb_nstatus; i++) { + for (i = 0; i < nstatus; i++) { + /* completion id 0 is never allocated (ident = slot + 1) */ + if (mbox.mb_completed[i] < 1) { + device_printf(sc->amr_dev, "bad completion id %d\n", + mbox.mb_completed[i]); + continue; + } /* get pointer to busy command */ idx = mbox.mb_completed[i] - 1; ac = sc->amr_busycmd[idx]; |