Firmware-facing frame->hdr.sge_count never reconciled with the validated user_ioc->sge_count: FW consumes stale SGEs (device DMA to freed ioctl buffers) and can read its SGL past the 1024-byte frame allocation
Summary
The driver substitutes kernel addresses into exactly user_ioc->sge_count SGEs but ships a verbatim user frame whose own count byte is never validated - a crafted packet makes the FW consume stale pool SGEs (device-mediated UAF of freed data buffers) and hdr.sge_count=255 walks 2040 bytes of SGL from the 1024-byte frame into adjacent kernel DMA memory. Static/likely (fw walk not replicable without the controller). Clamp-the-shipped-count fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4836 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | file | 2.5 KB | β raw | |
| verdict.json | file | 1.3 KB | view raw |
DF-4836 β frameβhdr.sge_count desync: firmware consumes stale SGEs beyond the driver-populated ones
- File:
sys/dev/raid/mrsas/mrsas_ioctl.c:205-225 - Severity: Medium β confidence likely (driver-side divergence certain; firmware-side consequence follows the MFI protocol but is not runtime-verifiable without an LSI controller)
- Not PoC-verified (no mrsas hardware in the guest; the divergence is statically provable, the device-side impact is not demonstrable).
Root cause
mrsas_passthru() validates user_ioc->sge_count against
MAX_IOCTL_SGE (16) (mrsas_ioctl.c:186) and populates exactly that many SGEs
into the frame at frame + user_ioc->sgl_off (:225-255). But the frame it
ships to the firmware is a verbatim copy of the user's frame
(:205 memcpy(cmd->frame, user_ioc->frame.raw, 2 * MEGAMFI_FRAME_SIZE),
128 bytes into the 1024-byte MRSAS_MFI_FRAME_SIZE allocation), and the frame
header's own sge_count byte (mrsas.h:1913, offset 0x07) is never validated
or overwritten β only context/pad_0 are fixed up (:206-207) and the SGL64
flag bits stripped (:208-209).
The firmware walks the frame's SGL per frame->hdr.sge_count (the whole MFI
passthrough design β mrsas_build_mptmfi_passthru() (mrsas.c:2820-2857) hands
the FW the raw frame_phys_addr as the SGL chain). If the caller sets
hdr.sge_count > user_ioc->sge_count:
- the extra SGEs the FW consumes are stale pool memory β zeroed only once
at pool creation (mrsas_ioctl.c:471), then carrying whatever previous
ioctls' SGLs left there: physical addresses of
bus_dmamem_freed ioctl data buffers; - with
hdr.sge_count = 255the FW reads 255Β·8 = 2040 bytes of SGL from a 1024-byte allocation β past the frame into adjacent kernel DMA memory.
Device-side consequences: DMA to/from freed kernel buffers (UAF by device) or
to garbage addresses parsed from adjacent memory β kernel memory corruption
mediated by the controller, triggered by the same root/operator ioctl surface
as DF-1917..DF-1919 (/dev/mrsasN is root:operator 0660).
Suggested fix
Clamp/overwrite the shipped frame header to the validated count after the copy:
cmd->frame->hdr.sge_count = user_ioc->sge_count; /* == validated <= 16 */
(for non-SGL frame types set it to the user's value only when the frame type
has an SGL; at minimum, reject hdr.sge_count > user_ioc->sge_count).
Artifacts
Static analysis only β no harness (the FW-side walk cannot be replicated honestly). See the finding markdown for the full trace.
Fix verification
not_testableConfirmed kernel references
Detail
Exploit chain
prior large ioctl leaves stale SGEs in the pooled frame -> crafted packet with low user_ioc count but high frame count -> FW DMAs to/from freed buffers / parses adjacent kernel DMA memory as SGL
Evidence (decisive lines)
findings/poc/DF-4836/ (this verdict; README documents the static chain)
Verdict
Static/likely (fw-side walk not honestly replicable without the controller): mrsas_passthru validates user_ioc->sge_count<=16 and substitutes kernel addresses into exactly that many SGEs, but the frame shipped to the FW is a verbatim user copy whose own hdr.sge_count byte is never validated or overwritten - a crafted packet (user_ioc->sge_count=1, frame.hdr.sge_count=16) makes the FW consume stale pool memory as SGEs (physical addresses of freed data buffers = device-mediated UAF), and hdr.sge_count=255 reads 2040 bytes of SGL from a 1024-byte frame allocation (past the frame into adjacent kernel DMA memory). Root/operator ioctl surface (same as DF-1917..1919). Clamp-the-shipped-count fix.
No comments yet.