β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3674

mpt_scsi_reply_handler / mpt_scsi_tgt_reply_handler dereference req with no NULL check while mpt_intr dispatches handlers with req=NULL on out-of-range IOC context

Summary

mpt_intr() looks up the request from IOC-owned context and, when req_index >= MPT_MAX_REQUESTS, only WARNs - then calls the handler anyway with req=NULL. mpt_scsi_reply_handler immediately evaluates req->state and mpt_scsi_tgt_reply_handler computes MPT_TGT_STATE(mpt, req) before any check, while the sibling mpt_sata_pass_reply_handler in the same file defends with 'if (req != NULL)' - the asymmetry proves the omission. A faulty/malicious IOC (or corrupted reply DMA) yields a NULL-deref panic. Shared with upstream FreeBSD head (verified). IOC-firmware-defect/malicious-hardware class: deterministic panic on a single crafted reply.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3674 Β· 6 files
FileTypeDescriptionSize
README.md β€” 438 B ↓ raw
VERDICT.md β€” 3.2 KB ↓ raw
fix.diff β€” 744 B view raw
env.txt β€” 281 B view raw
verdict.json β€” 2.2 KB view raw
manifest.json β€” 633 B view raw

DF-3674 β€” NULL request dereference in mpt_scsi_reply_handler / mpt_scsi_tgt_reply_handler

Severity: Low (IOC-driven kernel NULL deref / panic). No userland trigger exists β€” the NULL req comes from an IOC reply whose context index exceeds the request pool (mpt.c:812-820 dispatches anyway). See VERDICT.md.

Build/run: none applicable (interrupt-path bug); guest has no MPT HBA (env.txt). Upstream FreeBSD shares the missing guard.

VERDICT.md
↓ download raw

DF-3674 β€” mpt_scsi_reply_handler / mpt_scsi_tgt_reply_handler dereference req with no NULL check

Severity: Low β€” IOC-triggered NULL deref (panic); source-certain; runtime-gated on MPT hardware.

Root cause

mpt_intr() looks the request up from the IOC-supplied context and, when the index is out of pool range, only WARNs β€” then dispatches the handler with req == NULL:

/* sys/dev/disk/mpt/mpt.c:812-820 */
        req_index = MPT_CONTEXT_TO_REQI(ctxt_idx);
        if (req_index < MPT_MAX_REQUESTS(mpt)) {
            req = &mpt->request_pool[req_index];
        } else {
            mpt_prt(mpt, "WARN: mpt_intr index == %d (reply_desc =="
                " 0x%x)\n", req_index, reply_desc);
        }
        ...
        free_rf = mpt_reply_handlers[cb_index](mpt, req, ...);

ctxt_idx comes from IOC-owned memory: reply_frame->MsgContext for address replies (mpt.c:744) or the masked context word for MPI_CONTEXT_REPLY_TYPE_SCSI_INIT context replies (mpt.c:752-755).

The SCSI I/O handler then dereferences req before any validation:

/* sys/dev/disk/mpt/mpt_cam.c:2649 */
    if (req->state == REQ_STATE_FREE) {          /* NULL deref */

and the target handler likewise:

/* sys/dev/disk/mpt/mpt_cam.c:5204-5208 */
    if (reply_frame == NULL) {
        mpt_tgt_state_t *tgt = MPT_TGT_STATE(mpt, req);   /* deref */

The sibling handler in the same file does defend:

/* sys/dev/disk/mpt/mpt_cam.c:874-878 */
mpt_sata_pass_reply_handler(...)
{
    if (req != NULL) {            /* <-- the missing pattern */

MPT_CONTEXT_TO_REQI masks a 24-bit IOC-controlled field, so any IOC reply with a context whose request index is β‰₯ 512 (MPT_MAX_REQUESTS) while the handler byte selects scsi_io/tgt produces a NULL-deref panic. That is a firmware defect / malicious-IOC / bus-DMA-corruption scenario, identical in threat class to the pass-1 findings DF-1161/DF-2537 but with a weaker (NULL) primitive.

Upstream FreeBSD head has the same missing guard (verified) β€” a shared bug worth fixing in both trees.

Fix

--- a/sys/dev/disk/mpt/mpt_cam.c
+++ b/sys/dev/disk/mpt/mpt_cam.c
@@ -2646,6 +2646,11 @@ mpt_scsi_reply_handler(struct mpt_softc *mpt, request_t *req,
    MSG_SCSI_IO_REQUEST *scsi_req;
    union ccb *ccb;

+   if (req == NULL) {
+       mpt_prt(mpt, "mpt_scsi_reply_handler: NULL request\n");
+       return (TRUE);
+   }
+
    if (req->state == REQ_STATE_FREE) {
        mpt_prt(mpt, "mpt_scsi_reply_handler: req already free\n");
        return (TRUE);
@@ -5201,6 +5206,12 @@ mpt_scsi_tgt_reply_handler(struct mpt_softc *mpt, request_t *req,
    U16 status;

+   if (req == NULL) {
+       mpt_prt(mpt, "mpt_scsi_tgt_reply_handler: NULL request\n");
+       return (TRUE);
+   }
+
    if (reply_frame == NULL) {

(kept verbatim in fix.diff.)

Guest verdict

inconclusive β€” mpt compiled into X86_64_GENERIC (line 93) but zero LSI PCI devices exist on the QEMU guest, so no IOC replies are ever dispatched. Source-level certainty: certain (dispatch-with-NULL is provable at mpt.c:812-820).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Guard-only diff authored (fix.diff); cannot be exercised without MPT hardware on the guest.

fix.diff; env.txt
↓ fix.diffper-fix-DF-3674

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (path:line proof incl. the sibling-handler contrast); fix.diff (guards mirroring the sata_pass handler); env.txt (no MPT hardware)

PoC changes

n/a (no PoC β€” interrupt-path, IOC-gated)

Verified recommended fix

Add 'if (req == NULL) return (TRUE);' guards to mpt_scsi_reply_handler and mpt_scsi_tgt_reply_handler, mirroring mpt_sata_pass_reply_handler

Verdict

Source-certain NULL-deref: mpt_intr() dispatches reply handlers with req=NULL when the IOC-supplied context index exceeds the 512-entry request pool (mpt.c:812-820 WARN path still calls the handler); mpt_scsi_reply_handler dereferences req->state immediately (mpt_cam.c:2649) and mpt_scsi_tgt_reply_handler computes MPT_TGT_STATE(mpt, req) before any NULL check (mpt_cam.c:5204-5208), while the sibling mpt_sata_pass_reply_handler defends with 'if (req != NULL)' (mpt_cam.c:878) β€” the asymmetry proves the omission. Trigger requires a faulty/malicious IOC or corrupted reply DMA (hardware trust gap), same threat class as pass-1 DF-1161/DF-2537 but weaker primitive (NULL deref -> panic, not OOB). Not executable on the audit guest: no LSI PCI devices, mpt never attaches. Upstream FreeBSD head has the same missing guards.