β¬’ 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 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 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 5.7 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 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.