# DF-1162 — mpt_fc_els_reply_handler ABTS: tgt_cmd_ptrs[rx_id] bound mismatch (source-only verification)

## Verdict: REPRODUCED at source level (latent in compiled-in mpt(4); not triggerable without LSI-Logic MPT FC HBA)

## Mechanism

`mpt(4)` is statically compiled into `X86_64_GENERIC`
(`sys/config/X86_64_GENERIC:93`). The FC ABTS (Abort Sequence) handler runs
only when `mpt->is_fc` is true (set by the FC HBA attachment).

`tgt_cmd_ptrs` is allocated as **`max = min(MPT_MAX_REQUESTS/2,
mpt_max_tgtcmds)`** entries:

```c
/* sys/dev/disk/mpt/mpt_cam.c:4251-4256 */
max = MPT_MAX_REQUESTS(mpt) >> 1;
if (max > mpt->mpt_max_tgtcmds) {
    max = mpt->mpt_max_tgtcmds;
}
mpt->tgt_cmd_ptrs =
    kmalloc(max * sizeof (request_t *), M_DEVBUF, M_NOWAIT | M_ZERO);
```

But the ABTS path bounds-checks `rx_id` against `mpt_max_tgtcmds`, **not**
against `max` / `tgt_cmds_allocated`:

```c
/* sys/dev/disk/mpt/mpt_cam.c:3004-3010 */
if (rx_id >= mpt->mpt_max_tgtcmds) {           /* wrong bound */
    mpt_prt(mpt, "Bad RX_ID 0x%x\n", rx_id);
} else if (mpt->tgt_cmd_ptrs == NULL) {
    mpt_prt(mpt, "No TGT CMD PTRS\n");
} else {
    tgt_req = mpt->tgt_cmd_ptrs[rx_id];         /* OOB if max < rx_id < mpt_max_tgtcmds */
}
```

- `mpt_max_tgtcmds` is an alias for `port_facts[0].MaxPostedCmdBuffers`
  (`sys/dev/disk/mpt/mpt.h:566`) — a 16-bit IOC facts field.
- `MPT_MAX_REQUESTS/2` is the per-controller request pool halved.
- **When `MaxPostedCmdBuffers > MPT_MAX_REQUESTS/2`**, `tgt_cmd_ptrs` is
  capped at `MPT_MAX_REQUESTS/2` but the bound check still allows up to
  `mpt_max_tgtcmds`. Any ABTS with `RX_ID` in the half-open interval
  `[tgt_cmds_allocated, mpt_max_tgtcmds)` passes the check and reads an OOB
  pointer from beyond the array.

**Note the existing correct guard in `mpt.c:765`** for the analogous target
context-reply path: `if (ctxt_idx >= mpt->tgt_cmds_allocated)`. The ABTS path
in `mpt_cam.c:3004` does not use this correct bound.

**Effect:** heap OOB read → attacker-controlled pointer dereferenced as
`request_t *tgt_req` → `mpt_abort_target_cmd(mpt, tgt_req)`
(`mpt_cam.c:3046`), which then dereferences `MPT_TGT_STATE(mpt, tgt_req)`
fields. Crash or memory corruption.

## Trigger reachability on this guest

- No LSI-Logic MPT-Fusion FC HBA is present in the QEMU guest. The mpt(4)
  driver attaches to no FC device, so `mpt->is_fc` is never set and the
  ABTS branch (`rctl == ABTS && type == 0`) of `mpt_fc_els_reply_handler`
  is never entered.

## Fix

`fix.diff` replaces the wrong bound with the allocation-correct one:
```c
if (rx_id >= mpt->tgt_cmds_allocated) {        /* matches mpt.c:765 + mpt_cam.c:4283 */
    mpt_prt(mpt, "Bad RX_ID 0x%x (max %u)\n", rx_id,
        mpt->tgt_cmds_allocated);
} else if (mpt->tgt_cmd_ptrs == NULL) {
    ...
```
This makes the ABTS check identical in form to the existing
`tgt_cmds_allocated` guard in `mpt.c:765` and consistent with the allocation
size recorded at `mpt_cam.c:4283`.

## Build verification of the fix

`mpt.ko` built successfully from the patched source on the guest:
```
--- mpt_cam.o ---
cc  -O2 -pipe ... -Werror -DKLD_MODULE ... -c mpt_cam.c
--- mpt.ko ---
cc -Wl,--build-id=sha1 -nostdlib ... -o mpt.ko mpt.o mpt_cam.o mpt_debug.o mpt_pci.o mpt_raid.o mpt_user.o
BUILD_EXIT=0
```
Full output: `build.log`. The fix is build-clean.

## Fix-validation status

`not_testable` — requires an LSI-Logic MPT-Fusion FC HBA configured such that
`MaxPostedCmdBuffers > MPT_MAX_REQUESTS/2`, plus an adjacent FC peer sending
ABTS frames with crafted RX_ID. The guest has neither. We confirmed the fix
**applies cleanly** and the patched module **compiles**; the change is a
single-token correction (replacing one identifier with the allocation-correct
one already used in the same driver at `mpt.c:765`).
