MPT_TAG_2_REQ production macro indexes tgt_cmd_ptrs[tag_id>>18] with no bounds check β user-controlled XPT_CONT_TARGET_IO tag_id gives heap OOB read + wild request_t* dereference
Summary
The tag lookup used by every target-mode CTIO path is a raw array access in production kernels: '#define MPT_TAG_2_REQ(mpt, tag) mpt->tgt_cmd_ptrs[tag >> 18]'. The bounds-checked variant exists only under #ifdef INVARIANTS - DIAGNOSTIC-only hardening proving the authors knew the index needs validation. csio.tag_id is a u_int32 fully controlled by the pass(4) caller (XPT_CONT_TARGET_IO is not XPT_FC_XPT_ONLY; xpt_merge_ccb copies the payload), so tag_id>>18 spans 0..16383 against tgt_cmd_ptrs[] with at most 256 entries. The fetched garbage is immediately dereferenced as request_t* via MPT_TGT_STATE -> req->req_vbuf then switched on. Operator-group local user on a host with an mpt FC adapter in target role (no IOC misbehaviour needed): heap OOB read up to ~129KB past a <=2KB allocation, then double-deref -> wild kernel read -> panic; with heap grooming the default branch's mpt_tgt_dump_req_state prints attacker-addressed kernel words to the console -> arbitrary-address read oracle.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3672 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.3 KB | β raw | |
| VERDICT.md | β | 4.8 KB | β raw | |
| mpt_tag_oob.c | β | 2.9 KB | view raw | |
| build.sh | β | 89 B | view raw | |
| run.sh | β | 141 B | view raw | |
| env.txt | β | 281 B | view raw | |
| run.log | β | 281 B | view raw | |
| fix.diff | β | 3.6 KB | view raw | |
| verdict.json | β | 3.8 KB | view raw | |
| manifest.json | β | 935 B | view raw |
DF-3672 β MPT_TAG_2_REQ() indexes tgt_cmd_ptrs[tag>>18] with NO bounds check in production kernels
Severity: Medium (memcorrupt) β source-certain; runtime-gated on LSI FC target-mode hardware.
Build / Run (on a host that has an mpt(4) FC adapter in target role)
cc -O -o mpt_tag_oob mpt_tag_oob.c # DragonFly, with /usr/src present
./mpt_tag_oob /dev/passN # N = pass node on the mpt target-mode bus
Expected on vulnerable kernel: immediate kernel panic (wild request_t *
dereference) or, after heap grooming around the 2 KB tgt_cmd_ptrs
allocation, mpt_tgt_dump_req_state() printing attacker-chosen kernel
addresses/words to the console (req %p:%u index %u ... β mpt_cam.c:4371,
5187-5194).
Why this guest cannot run it
- Kernel
X86_64_GENERICcompiles mpt in (sys/config/X86_64_GENERIC:93, confirmed on the guest), but the QEMU guest has no LSI (vendor 0x1000) PCI device βpciconf -lv | grep -c vendor=0x1000= 0 β so no mpt softc ever attaches, no CAM bus registers, and no /dev/passN node on an mpt bus exists to submit the CCB through. - The trigger is a plain unprivileged (operator-group) ioctl once such hardware is present; no IOC misbehaviour is required β the user supplies the bad tag directly.
See env.txt, VERDICT.md, verdict.json.
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:
/* 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:
/* 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:
/* 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_idisu_int32_tin the CAM CCB payload. The kernel-side CCB payload is copied verbatim from userland byxpt_merge_ccb()(sys/bus/cam/cam_xpt.c:3888-3900) β payload afterccb_hdr(which is wherecsiolives 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_ptrsallocation so the OOB slot contains a chosen value;mpt_target_start_io()then takes thedefault:branch which callsmpt_tgt_dump_req_state(mpt, cmd_req)(mpt_cam.c:4368-4374β5187-5194), printingreq->serno,req->index,req->state, andMPT_TGT_STATEfields read from the attacker-chosen address to the console/log β an arbitrary-address read oracle (info-leak) feedingmpt->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")
- 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()frommpt_cam_ready()). - Target mode enabled (XPT_EN_LUN) so
tgt_cmd_ptrs[]is populated. - An open
/dev/passNon 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.
Fix verification
not_testablefix.diff authored against read-only sys/ (unconditional checked accessor + call-site guards). Cannot build-and-run a single-fix kernel validation: the bug cannot be triggered even on the baseline guest (no MPT hardware), so baseline-vs-patched comparison is meaningless here. fix.diff is compile-consistent by inspection.
fix.diff; absence of hardware documented in env.txt
Confirmed kernel references
Detail
Exploit chain
operator-group user -> open /dev/passN on mpt FC target-mode bus -> CAMIOCOMMAND XPT_CONT_TARGET_IO with csio.tag_id=0xFFFC0000 -> xpt_merge_ccb copies payload -> mpt_target_start_io -> tgt_cmd_ptrs[1023] OOB read (array <=256 entries) -> garbage request_t* -> MPT_TGT_STATE double-deref -> wild kernel read -> panic; groomed heap variant -> default: branch -> mpt_tgt_dump_req_state prints attacker-addressed words to console (read oracle)
Evidence (decisive lines)
VERDICT.md (full path:line trace); env.txt + run.log (guest: mpt in GENERIC, 0 LSI PCI devices); mpt_tag_oob.c (trigger); fix.diff (unconditional bounds-checked accessor + NULL guards)
PoC changes
n/a (pass-1 seed absent; authored from scratch: single CAMIOCOMMAND with XPT_CONT_TARGET_IO and tag_id 0xFFFC0000)
Verified recommended fix
Make the bounds-checked mpt_tag_2_req() inline unconditional (return NULL on out-of-range tag / missing array / NULL slot) and NULL-guard every MPT_TAG_2_REQ call site, completing the CCB with CAM_REQ_CMP_ERR
Verdict
Source-certain heap OOB read: the production MPT_TAG_2_REQ macro (mpt.h:360) indexes mpt->tgt_cmd_ptrs[tag_id >> 18] with no bounds check while the checked variant exists only under INVARIANTS (mpt.h:951-963). tag_id is a fully user-controlled u_int32 in the XPT_CONT_TARGET_IO CCB payload which pass(4) forwards to mpt_target_start_io (mpt_cam.c:4355); tag_id>>18 spans 0..16383 against an array of at most 256 entries, then the fetched value is dereferenced as request_t* (MPT_TGT_STATE -> req->req_vbuf) -> wild kernel read/panic, or a groomed arbitrary-address read oracle via mpt_tgt_dump_req_state. Not executable on the audit guest: mpt is compiled into X86_64_GENERIC (line 93) but the QEMU guest has zero vendor-0x1000 PCI devices, so no mpt softc ever attaches and no pass node on an mpt bus exists. Precondition check + full reasoning in VERDICT.md; expected impact on real FC target-mode hardware: local panic/DoS (operator-group user), potential info-leak oracle.
No comments yet.