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