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

Unbounded sgl_off yields OOB SGE write into cmd->frame DMA allocation

Summary

mrsas_passthru L219-220 kern_sge32=(struct mrsas_sge32*)((unsigned long)cmd->frame+user_ioc->sgl_off) attacker-controlled u32 offset never validated against 1024-byte MRSAS_MFI_FRAME_SIZE. L254-255 writes up to 16 SGE entries (128 bytes) through that pointer. sgl_off>=896 writes past end of DMA-coherent allocation into adjacent kernel memory. Operator-group /dev/mrsas0 mode 0660 mrsas.c:790-792. Fix: validate sgl_off+sge_count*sizeof(sge32)<=MRSAS_MFI_FRAME_SIZE.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1917 Β· 14 files
FileTypeDescriptionSize
harness.c trigger-source userspace model of mrsas_passthru L219-255 OOB SGE write 4.9 KB view raw
fixcheck.c fix-validation models the patched predicate; rejects the 4 OOB vectors 2.0 KB view raw
build.sh build-script cc -O2 -o harness harness.c 65 B view raw
run.sh run-script ./harness 89 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 853 B view raw
fix_run.log run-log harness re-run on patched #1 kernel (system-healthy check) 935 B view raw
fixcheck.log fix-log fix predicate output on patched #1 kernel (4/6 rejected) 546 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 sgl_off + sge_count*sizeof(sge32) <= MFI_FRAME_SIZE 1.3 KB view raw
VERDICT.md verdict full verification narrative 6.0 KB ↓ raw
README.md readme PoC README 1.8 KB ↓ raw
README.md readme PoC README
↓ download raw

DF-1917 PoC

Trigger: pass sgl_off = 1020..0xFFFFFFFF and sge_count = 1..16 in an MRSAS_IOC_FIRMWARE_PASS_THROUGH ioctl on /dev/mrsas0. The driver writes up to 16 Γ— 8-byte SGE entries (mrsas_sge32 { u32 phys_addr; u32 length; }) at cmd->frame + sgl_off + 8*i without ever checking that sgl_off + 8*sge_count <= MRSAS_MFI_FRAME_SIZE (1024), so the writes spill past the 1024-byte bus_dmamem_alloc'd DMA frame into adjacent kernel heap.

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. The QEMU audit guest emulates no MegaRAID SAS HBA (verified: pciconf -lv lists none, /dev/mrsas* absent), so mrsas_passthru is unreachable at runtime; and even on a host with the HBA, maxx (uid 1001) is not in operator (verified), so the unprivileged ceiling is "operator-group member". No uid=0 claim β€” the bug is real, the live path is closed on this guest.

Build

cc -O2 -o harness harness.c

Run

./harness

Expected output

The harness reproduces the verbatim buggy arithmetic of mrsas_ioctl.c:219-255 against a 1024-byte model of cmd->frame and prints, for each (sgl_off, sge_count) test vector, the number of bytes the SGE writes would land past the 1024-byte DMA allocation. In-bounds cases print 0; OOB cases print 4..128 (single SGE) up to 124 (16 SGEs at offset 1020). sgl_off = 0xFFFFFFFF shows the pointer-wrap variant.

Fix

See fix.diff: clamp sgl_off + sge_count * sizeof(struct mrsas_sge32) <= MRSAS_MFI_FRAME_SIZE immediately after reading user_ioc->sgl_off, return EINVAL otherwise.

VERDICT.md verdict full verification narrative
↓ download raw

DF-1917 β€” Verification Verdict

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

The unbounded-sgl_off OOB SGE write is confirmed at sys/dev/raid/mrsas/mrsas_ioctl.c:219-255. The harness reproduces the verbatim buggy arithmetic against a 1024-byte model of cmd->frame and shows that for every attacker-supplied sgl_off that would push sgl_off + 8*sge_count past MRSAS_MFI_FRAME_SIZE, the kernel writes attacker-supplied SGE bytes (phys_addr, length) past the 1024-byte DMA allocation into adjacent kernel heap.

Mechanism

// mrsas_ioctl.h:80-92  -- attacker-controlled u32 fields, no validation
struct mrsas_iocpacket {
    u_int16_t host_no;
    u_int16_t __pad1;
    u_int32_t sgl_off;     // <-- attacker u32
    u_int32_t sge_count;
    ...
};

// mrsas_ioctl.c:219-220  -- u32 offset used verbatim
kern_sge32 = (struct mrsas_sge32 *)
    ((unsigned long)cmd->frame + user_ioc->sgl_off);

// mrsas_ioctl.c:225-255  -- up to 16 SGEs written at frame+sgl_off+8*i
for (i = 0; i < user_ioc->sge_count; i++) {
    ...
    kern_sge32[i].phys_addr = (u_int32_t)ioctl_data_phys_addr[i];
    kern_sge32[i].length    = user_ioc->sgl[i].iov_len;
}

cmd->frame is bus_dmamem_alloc'd at exactly MRSAS_MFI_FRAME_SIZE (=1024 bytes; mrsas.h:876, mrsas.c:451-462). mrsas_sge32 is 8 bytes (mrsas.h:1883-1886, #pragma pack(1)). MAX_IOCTL_SGE=16 (mrsas_ioctl.h:66). So a single ioctl can land up to 16Γ—8 = 128 bytes of attacker-offset writes; with sgl_off=0x3FC and sge_count=16, 124 of those bytes go past byte 1024.

sge_count IS bounds-checked at mrsas_ioctl.c:186 (≀ 16), but sgl_off is never validated anywhere.

Harness evidence (run.log)

DF-1917: mrsas_passthru unbounded sgl_off (mrsas_ioctl.c:219-255)
  sgl_off=0x00000080 sge_count= 1 -> 0 OOB bytes (in-bounds)
  sgl_off=0x000003f8 sge_count= 1 -> 0 OOB bytes (in-bounds)
  sgl_off=0x000003fc sge_count= 1 -> 4 OOB bytes (OOB WRITE past 1024-byte frame)
  sgl_off=0x00000400 sge_count= 1 -> 8 OOB bytes (OOB WRITE past 1024-byte frame)
  sgl_off=0x000003fc sge_count=16 -> 124 OOB bytes (OOB WRITE past 1024-byte frame)
  sgl_off=0xffffffff sge_count= 1 -> 8 OOB bytes (OOB WRITE past 1024-byte frame)

Why no live trigger / Phase-6 hard blocker

The mrsas cdev is created only by mrsas_attach (sys/dev/raid/mrsas/mrsas.c:790-792):

sc->mrsas_cdev = make_dev(&mrsas_ops, device_get_unit(dev), UID_ROOT,
    GID_OPERATOR, (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP), "mrsas%u",
    device_get_unit(dev));

β€” and mrsas_attach runs only when the PCI probe finds an LSI MegaRAID SAS HBA (sys/dev/raid/mrsas/mrsas.c:3668-3669: DRIVER_MODULE(mrsas, pci, ...)). The QEMU audit guest emulates no such HBA (verified: pciconf -lv lists none; /dev/mrsas* absent), so mrsas_passthru is unreachable at runtime on this guest. Additionally the node would be mode 0660 root:operator, and maxx (uid 1001) is not in operator (verified: pw groupshow operator β‡’ operator:*:5:root), so the unprivileged ceiling is "operator-group member". Valid Phase-6 hard blocker (per AGENT.md: "...dead/unreachable at runtime on this guest...prove the primitive at the object/harness level...").

Exploit chain

Not applicable on this guest (no HBA β†’ no /dev/mrsas* β†’ no mrsas_passthru call from maxx). On real hardware with the HBA, an operator-group member would have an arbitrary-offset (limited to low-entropy phys_addr/length content) kernel heap write relative to a 1024-byte DMA allocation; on noinv this would be silently corrupting, on default GENERIC (INVARIANTS ON) the slab poison/magic checks in kern_slaballoc.c would likely catch cross-zone reuse and panic (DoS). A full uid=0 chain would require (a) operator-group membership, (b) info-leak of the kernel-heap layout adjacent to cmd->frame, (c) a victim object with a function pointer / ucred * / refcount landing in the spill zone β€” feasible in principle on real hardware but not demonstrable on this guest.

PoC changes

  • Added harness.c: verbatim buggy arithmetic against a 1024-byte frame model; 6 test vectors covering in-bounds, edge, OOB, max-spill, pointer-wrap.
  • Added fixcheck.c: models the patched predicate from fix.diff (mrsas_ioctl.c:242-246) and shows it rejects the 4 OOB vectors.
  • Added fix.diff: validates sgl_off + sge_count*sizeof(mrsas_sge32) <= MRSAS_MFI_FRAME_SIZE before computing kern_sge32, returning EINVAL otherwise.

Fix validation (Phase 8)

fix.diff was applied as part of a combined patch (with DF-1918 and DF-1919, same file) to /usr/src on the with-src snapshot. Kernel rebuilt cleanly (make -j6 nativekernel KERNCONF=X86_64_GENERIC, NK_DONE rc=0; mrsas_ioctl.o recompiled without warnings). The single-fix kernel installed as /boot/kernel/kernel (sha256 c8c9a25c98bc8e06c300820f141d8d1a3e89dcda21c7d2585b36bf8ddb72f064) and booted successfully as DragonFly 6.5-DEVELOPMENT #1: Mon Jul 20 20:05:20 UTC 2026. The harness re-ran on the patched kernel (system healthy).

Because /dev/mrsas0 does not exist on this guest, the kernel-side fix could not be exercised via the live syscall path. fixcheck.c models the patched predicate directly: it rejects 4/6 vectors (the 4 that caused OOB), accepts the 2 in-bounds vectors β€” proving the patched predicate closes the OOB inputs.

  • baseline (#0 5dc83dac…): harness shows 4 vectors with OOB write.
  • patched (#1 c8c9a25c…): harness unchanged (it is a userspace model); fixcheck shows the same 4 vectors now REJECTED (EINVAL).

fix_status: fixed β€” the patched predicate provably rejects every input vector that previously caused an OOB write, and the patched kernel is bootable and stable.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

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

baseline 4 OOB vectors; patched 4/6 rejected (EINVAL).
↓ fix.diff6.5-DEV #1 c8c9a25c (combined DF-1917+1918+1919 kernel)

Confirmed kernel references

Detail

Exploit chain

Blocked (no LSI MegaRAID SAS HBA in QEMU; /dev/mrsas* absent; node would be 0660 root:operator, maxx not in operator). Primitive: arbitrary-offset 8-byte-SGE OOB write up to 128 bytes vs 1024-byte DMA alloc.

Evidence (decisive lines)

sgl_off=0x3fc sge_count=16 -> 124 OOB bytes; sgl_off=0xffffffff -> 8 OOB bytes. fixcheck: 4/6 vectors rejected.

PoC changes

Created harness.c, fixcheck.c, fix.diff (sgl_off + sge_count*sizeof <= MRSAS_MFI_FRAME_SIZE check).

Verified recommended fix

At mrsas_ioctl.c:242-246: reject EINVAL when sgl_off > MRSAS_MFI_FRAME_SIZE OR sgl_off + sge_count*sizeof(mrsas_sge32) > MRSAS_MFI_FRAME_SIZE (64-bit math).

Verdict

REPRODUCED source+harness. mrsas_passthru at mrsas_ioctl.c:219-220 computes kern_sge32 = cmd->frame + user_ioc->sgl_off (u32 attacker-controlled, no validation), then writes 16*8 SGE bytes. cmd->frame is bus_dmamem_alloc'd at MRSAS_MFI_FRAME_SIZE=1024. Harness shows 4/6 vectors OOB write 4..124 bytes.