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

Unbounded sense_off yields OOB pointer-sized write into cmd->frame

Summary

mrsas_passthru L291-293 sense_ptr=(unsigned long*)((unsigned long)cmd->frame+user_ioc->sense_off) attacker-controlled u32 never validated. *sense_ptr=ioctl_sense_phys_addr writes 8 bytes at arbitrary offset relative to 1024-byte frame. sense_off>=1016 spills outside allocation. Operator-group. Fix: validate sense_off<=MRSAS_MFI_FRAME_SIZE-sizeof(unsigned long).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1918 Β· 14 files
FileTypeDescriptionSize
harness.c trigger-source userspace model of mrsas_passthru L291-293 OOB sense_ptr store 4.0 KB view raw
fixcheck.c fix-validation models the patched predicate; rejects the 4 OOB vectors 1.5 KB view raw
build.sh build-script cc -O2 -o harness harness.c 142 B view raw
run.sh run-script ./harness 109 B view raw
build.log build-log build output on guest 82 B view raw
run.log run-log harness output on baseline #0 kernel 807 B view raw
fix_run.log run-log harness re-run on patched #1 kernel (system-healthy check) 889 B view raw
fixcheck.log fix-log fix predicate output on patched #1 kernel (4/6 rejected) 480 B view raw
fix_build.log build-log full nativekernel build log (combined patch); NK_DONE rc=0 5.6 MB ↓ download
fix_env.txt environment patched #1 kernel: kern.version, uname, /dev/mrsas absent 316 B view raw
env.txt environment baseline guest env: uname, cc, /dev/mrsas absent 333 B view raw
fix.diff suggested-fix validate sense_off <= MFI_FRAME_SIZE - sizeof(unsigned long) 1.3 KB view raw
VERDICT.md verdict full verification narrative 4.2 KB ↓ raw
README.md readme PoC README 1.4 KB ↓ raw
README.md readme PoC README
↓ download raw

DF-1918 PoC

Trigger: pass sense_off = 1017..0xFFFFFFFF and any non-zero sense_len in an MRSAS_IOC_FIRMWARE_PASS_THROUGH ioctl on /dev/mrsas0. The driver computes sense_ptr = (unsigned long *)(cmd->frame + sense_off) and unconditionally stores the 8-byte physical address of the sense buffer at that offset, without ever checking sense_off + sizeof(unsigned long) <= MRSAS_MFI_FRAME_SIZE (1024). sense_off >= 1017 spills 1..8 bytes past the 1024-byte DMA frame; sense_off = 0xFFFFFFF8 wraps the pointer to cmd->frame - 8.

Preconditions

  • An LSI MegaRAID SAS HBA present (mrsas_attach creates the cdev).
  • /dev/mrsas0 is created mode 0660 root:operator (sys/dev/raid/mrsas/mrsas.c:790-792), so the caller must be root or in the operator group.

Phase-6 hard blocker on this guest. Same as DF-1917 β€” no MegaRAID SAS HBA in the QEMU audit guest (verified), so mrsas_passthru is unreachable, and maxx is not in operator anyway.

Build

cc -O2 -o harness harness.c

Run

./harness

Expected output

For each sense_off test vector, the harness prints the byte count of the 8-byte *sense_ptr = phys_addr store that lands past the 1024-byte frame model. In-bounds cases print 0; OOB cases print 1..8.

Fix

See fix.diff: after computing user_ioc->sense_off, require user_ioc->sense_off <= MRSAS_MFI_FRAME_SIZE - sizeof(unsigned long) before issuing the store.

VERDICT.md verdict full verification narrative
↓ download raw

DF-1918 β€” Verification Verdict

Verdict: REPRODUCED (source-confirmed + sense_off-OOB-harness) β€” Phase-6 hard blocker (no HBA)

The unbounded-sense_off OOB pointer-sized write is confirmed at sys/dev/raid/mrsas/mrsas_ioctl.c:291-293. The harness reproduces the verbatim buggy store against a 1024-byte model of cmd->frame and shows that for every attacker-supplied sense_off that would push sense_off + sizeof(unsigned long) past MRSAS_MFI_FRAME_SIZE, the kernel writes 1..8 bytes of ioctl_sense_phys_addr past the 1024-byte DMA allocation into adjacent kernel heap.

Mechanism

// mrsas_ioctl.c:267  -- attacker must also set sense_len > 0 (trivial)
if (user_ioc->sense_len) {
    ...
    // mrsas_ioctl.c:291-293  -- u32 offset used verbatim
    sense_ptr = (unsigned long *)
        ((unsigned long)cmd->frame + user_ioc->sense_off);
    *sense_ptr = ioctl_sense_phys_addr;       // <-- 8-byte unchecked store
}

cmd->frame is 1024 bytes; sizeof(unsigned long) is 8 on amd64; sense_off is u_int32_t (mrsas_ioctl.h:85), never validated. With sense_off=1017 the last 1 of 8 bytes spills; with sense_off=1024 all 8 bytes spill; with sense_off=0xFFFFFFF8 the pointer wraps to cmd->frame - 8.

The written VALUE is the kernel physical address of the just-allocated sense DMA buffer (low-entropy, attacker cannot directly choose it), so this primitive is naturally a write-what-where of a fixed value at an attacker-chosen offset β€” still very serious (corrupt the right adjacent object and you control its interpretation), but harder to weaponize alone than DF-1919's attacker-bytes overflow.

Harness evidence (run.log)

DF-1918: mrsas_passthru unbounded sense_off (mrsas_ioctl.c:291-293)
  sense_off=0x00000080 -> 0/8 bytes past 1024-byte frame (in-bounds)
  sense_off=0x000003f8 -> 0/8 bytes past 1024-byte frame (in-bounds)
  sense_off=0x000003f9 -> 1/8 bytes past 1024-byte frame (OOB WRITE)
  sense_off=0x00000400 -> 8/8 bytes past 1024-byte frame (OOB WRITE)
  sense_off=0x00001400 -> 8/8 bytes past 1024-byte frame (OOB WRITE)
  sense_off=0xfffffff8 -> 8/8 bytes past 1024-byte frame (OOB WRITE)

Why no live trigger / Phase-6 hard blocker

Same as DF-1917. /dev/mrsas0 is created only by mrsas_attach (mrsas.c:790-792), which runs only when an LSI MegaRAID SAS HBA is probed (mrsas.c:3668-3669). The QEMU audit guest has no such HBA (verified: pciconf -lv lists none; /dev/mrsas* absent), and the node would be 0660 root:operator while maxx (uid 1001) is not in operator. Valid Phase-6 hard blocker (path dead at runtime on this guest; primitive proved at harness level).

Exploit chain

Not applicable on this guest. On real hardware with the HBA, an operator-group member would have an arbitrary-offset 8-byte write of a fixed low-entropy kernel-physical address relative to cmd->frame. Useful as a write-what-where tool combined with an info leak (defeats KASLR / leaks the sense-buffer phys addr to control the value); alone it is a corruption primitive. Not demonstrable end-to-end on this guest.

PoC changes

  • Added harness.c: verbatim buggy store against a 1024-byte frame model; 6 test vectors covering in-bounds, edge, 1..8-byte spill, pointer-wrap.
  • Added fixcheck.c: models the patched predicate from fix.diff (mrsas_ioctl.c:323-324) and shows it rejects the 4 OOB vectors.
  • Added fix.diff: validates sense_off <= MRSAS_MFI_FRAME_SIZE - sizeof(unsigned long) before the store, returning EINVAL otherwise.

Fix validation (Phase 8)

fix.diff applied as part of the combined patch (with DF-1917 and DF-1919, same file). Kernel rebuilt cleanly, single-fix kernel installed as /boot/kernel/kernel (sha256 c8c9a25c98bc8e06c300820f141d8d1a3e89dcda21c7d2585b36bf8ddb72f064), booted as 6.5-DEVELOPMENT #1: Mon Jul 20 20:05:20 UTC 2026. fixcheck.c shows the patched predicate rejects 4/6 vectors (the 4 that caused OOB), accepts the 2 in-bounds vectors.

  • baseline (#0 5dc83dac…): harness shows 4 vectors with OOB write.
  • patched (#1 c8c9a25c…): fixcheck shows the same 4 vectors now REJECTED (EINVAL).

fix_status: fixed β€” patched predicate closes the OOB inputs; patched kernel is bootable and stable.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. Combined kernel rc=0; fixcheck rejects 4/6.

baseline 4 OOB; patched 4/6 rejected.
↓ fix.diff6.5-DEV #1 c8c9a25c

Confirmed kernel references

Detail

Exploit chain

Blocked (same as DF-1917). Primitive: arbitrary-offset 8-byte pointer write of fixed kernel-phys value.

Evidence (decisive lines)

sense_off=0x400 -> 8 OOB bytes; 0xfffffff8 -> wraps to frame-8. fixcheck rejects 4/6.

PoC changes

harness.c, fixcheck.c, fix.diff (sense_off <= MRSAS_MFI_FRAME_SIZE - sizeof(unsigned long) check).

Verified recommended fix

At mrsas_ioctl.c:323-324: reject EINVAL when sense_off > MRSAS_MFI_FRAME_SIZE - sizeof(unsigned long).

Verdict

REPRODUCED source+harness. mrsas_passthru at mrsas_ioctl.c:291-293 sense_ptr = cmd->frame + user_ioc->sense_off (u32 attacker-controlled, no validation), then unconditionally stores *sense_ptr = ioctl_sense_phys_addr (8 bytes). sense_off=0xFFFFFFF8 wraps pointer to cmd->frame-8.