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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4836 Β· 2 files
FileTypeDescriptionSize
README.md file 2.5 KB ↓ raw
verdict.json file 1.3 KB view raw
README.md file
↓ download 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 = 255 the 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_testable
per-fix-DF-4836

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