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)
PoC verification
Evidence pack
findings/poc/DF-1917 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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/mrsas0is created mode0660 root:operator(sys/dev/raid/mrsas/mrsas.c:790-792), so the caller must berootor in theoperatorgroup.
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.
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 fromfix.diff(mrsas_ioctl.c:242-246) and shows it rejects the 4 OOB vectors. - Added
fix.diff: validatessgl_off + sge_count*sizeof(mrsas_sge32) <= MRSAS_MFI_FRAME_SIZEbefore computingkern_sge32, returningEINVALotherwise.
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 nowREJECTED (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
fixedVALIDATED. Combined kernel builds rc=0; fixcheck rejects 4/6 OOB vectors.
baseline 4 OOB vectors; patched 4/6 rejected (EINVAL).
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- r
- s
- a
- s
- /
- m
- r
- s
- a
- s
- _
- i
- o
- c
- t
- l
- .
- c
- :
- 2
- 1
- 9
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- r
- s
- a
- s
- /
- m
- r
- s
- a
- s
- _
- i
- o
- c
- t
- l
- .
- c
- :
- 2
- 2
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- r
- s
- a
- s
- /
- m
- r
- s
- a
- s
- _
- i
- o
- c
- t
- l
- .
- h
- :
- 8
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- r
- s
- a
- s
- /
- m
- r
- s
- a
- s
- .
- h
- :
- 8
- 7
- 6
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.
No comments yet.