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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3672 Β· 10 files
FileTypeDescriptionSize
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_GENERIC compiles 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.

VERDICT.md
↓ download raw

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

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-3672

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.