# DF-3672 — VERDICT

**status: inconclusive (execution-gated: no MPT hardware on the audit guest; source-level certainty: certain)**

## Root cause

`mpt_target_start_io()` — the XPT_CONT_TARGET_IO entry point — resolves the
kernel-side target request from the CCB's `tag_id`:

```c
/* sys/dev/disk/mpt/mpt_cam.c:4354-4356 */
struct ccb_scsiio *csio = &ccb->csio;
request_t *cmd_req = MPT_TAG_2_REQ(mpt, csio->tag_id);
mpt_tgt_state_t *tgt = MPT_TGT_STATE(mpt, cmd_req);
```

In production kernels the macro is a raw array access:

```c
/* sys/dev/disk/mpt/mpt.h:357-361 */
#ifdef	INVARIANTS
#define	MPT_TAG_2_REQ(a, b)		mpt_tag_2_req(a, (uint32_t) b)
#else
#define	MPT_TAG_2_REQ(mpt, tag)		mpt->tgt_cmd_ptrs[tag >> 18]
#endif
```

and the bounds-checked inline exists **only** under INVARIANTS:

```c
/* sys/dev/disk/mpt/mpt.h:951-963 */
static __inline request_t *
mpt_tag_2_req(struct mpt_softc *mpt, uint32_t tag)
{
	uint16_t rtg = (tag >> 18);
	KASSERT(rtg < mpt->tgt_cmds_allocated, ("bad tag %d", tag));
	KASSERT(mpt->tgt_cmd_ptrs, ("no cmd backpointer array"));
	KASSERT(mpt->tgt_cmd_ptrs[rtg], ("no cmd backpointer"));
	return (mpt->tgt_cmd_ptrs[rtg]);
}
```

The authors demonstrably knew the index needs validation (DIAGNOSTIC-only
hardening), but every shipped kernel executes the unchecked macro.

## Attacker-controlled index

- `csio.tag_id` is `u_int32_t` in the CAM CCB payload. The kernel-side CCB
  payload is copied verbatim from userland by `xpt_merge_ccb()`
  (`sys/bus/cam/cam_xpt.c:3888-3900`) — payload after `ccb_hdr` (which is
  where `csio` lives for func XPT_CONT_TARGET_IO).
- pass(4) forwards any func code that is not `XPT_FC_XPT_ONLY`
  (`sys/bus/cam/scsi/scsi_pass.c:473-478`); XPT_CONT_TARGET_IO = `0x33 |
  XPT_FC_QUEUED | XPT_FC_USER_CCB` (`sys/bus/cam/cam_ccb.h`) is allowed.
- xpt_action routes it to the SIM: `mpt_action()` case XPT_CONT_TARGET_IO
  (`mpt_cam.c:3711-3712`) → `mpt_target_start_io()`.

So `tag_id >> 18` spans **[0, 16383]** while the array has
`tgt_cmds_allocated <= min(MPT_MAX_REQUESTS(mpt)>>1, mpt_max_tgtcmds) =
min(256, MaxPostedCmdBuffers)` entries — allocated as
`kmalloc(max * sizeof(request_t *))` at `mpt_cam.c:4255-4256`
(MPT_MAX_REQUESTS = 512, `mpt.h:821`). With `tag_id = 0xFFFC0000`, index
1023 reads ~6 KB past the allocation; the maximum miss is ~129 KB.

The fetched garbage is immediately used as a `request_t *`:
`MPT_TGT_STATE(mpt, cmd_req)` = `&((uint8_t *)cmd_req->req_vbuf)[...]`
(`mpt.h:363-364`) — a second dereference through the OOB-read pointer —
then `switch (tgt->state)` reads through it.

## Impact

- Typical: unmapped/wild pointer → kernel panic (local DoS from an
  operator-group user; no IOC misbehaviour needed).
- Groomed: spray heap after the <=2 KB `tgt_cmd_ptrs` allocation so the OOB
  slot contains a chosen value; `mpt_target_start_io()` then takes the
  `default:` branch which calls `mpt_tgt_dump_req_state(mpt, cmd_req)`
  (`mpt_cam.c:4368-4374` → `5187-5194`), printing `req->serno`, `req->index`,
  `req->state`, and `MPT_TGT_STATE` fields read *from the attacker-chosen
  address* to the console/log — an arbitrary-address read oracle
  (info-leak) feeding `mpt->tgt_cmd_ptrs[x]->...` confusion.
- The same unchecked macro is used in the target-assist DMA-map error/cancel
  paths: `mpt_cam.c:1343, 1619, 1652, 1746, 2006, 2039`.

## Trigger preconditions (why "inconclusive" and not "not_reproduced")

1. LSI Fusion-MPT **FC** adapter attached with target role
   (`mpt->is_fc && (mpt->role & MPT_ROLE_TARGET)`) — target commands only
   get posted for FC (`mpt_cam.c:268-276`, `mpt_add_target_commands()` from
   `mpt_cam_ready()`).
2. Target mode enabled (XPT_EN_LUN) so `tgt_cmd_ptrs[]` is populated.
3. An open `/dev/passN` on a device on that bus (operator group or root).

On the audit QEMU guest the driver is compiled in
(`sys/config/X86_64_GENERIC:93`, verified on guest) but **no vendor-0x1000
PCI device exists**, so (1) can never hold: the path is unreachable at
runtime here. The defect itself is proven by source: the production macro
has no check, the index is user-supplied, and the INVARIANTS twin proves
intent. Hence inconclusive-by-environment, certain-by-source.

## Fix

Make the safe accessor unconditional and NULL-guard the call sites
(see `fix.diff`).

## Evidence

- `env.txt` — guest probe: mpt in GENERIC, zero LSI PCI devices.
- `mpt_tag_oob.c` — the PoC that would trigger it on real hardware.
- `run.log` — the guest hardware/module presence probe output.

## 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.
