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

mrsas_passthru operates on the raw userland ioctl pointer: validation→use TOCTOU on sge_count walks past the 16-entry stack arrays (kernel stack OOB write + OOB stack reads); unmapped arg panics the kernel

Summary

The ioctl layer hands the raw a_data pointer to mrsas_passthru with no copyin - every 'validation' reads memory the caller can mutate concurrently; three loops re-read sge_count/sgl[] from user memory writing/reading the [16] stack arrays, and the copyout loop runs after the multi-ms FW command (a very wide race window). A second thread flips sge_count after validation -> controlled-offset kernel stack OOB write (8*i bytes past the arrays, value = kmalloc'd tag pointers) across saved registers/return address; the copyout loop discloses kernel stack values; an unmapped arg faults the kernel with no on-fault handler. Harness: guard-page SIGSEGV with 576+ canary slots clobbered + deterministic copyout OOB reads + clean fixed model. Root/operator (0660). Copyin-at-entry fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4834 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 3.8 KB ↓ raw
df4834_harness.c β€” 8.8 KB view raw
build.sh β€” 104 B view raw
run.sh β€” 89 B view raw
build.log β€” 99 B view raw
run.log β€” 1.4 KB view raw
env.txt β€” 242 B view raw
fix.diff β€” 1.2 KB view raw
manifest.json β€” 927 B view raw
verdict.json β€” 5.0 KB view raw

DF-4834 — mrsas_passthru() raw userland pointer: validation→use TOCTOU walks past 16-entry stack arrays

Root cause

mrsas_ioctl() (mrsas.c:1151) passes the raw userland a_data pointer into mrsas_passthru(). Every field access in the passthrough then reads/writes user memory directly, with no copyin:

  • mrsas_ioctl.c:186 validates user_ioc->sge_count > MAX_IOCTL_SGE (16) β€” a read of USER memory
  • mrsas_ioctl.c:225 for (i = 0; i < user_ioc->sge_count; i++) β€” re-reads USER memory, and the body writes &ioctl_data_tag[i], &ioctl_data_mem[i], &ioctl_data_phys_addr[i] β€” stack arrays declared [MAX_IOCTL_SGE] (=16) at mrsas_ioctl.c:153-156
  • mrsas_ioctl.c:307 the copyout loop re-reads sge_count/sgl[] a second time, after the multi-millisecond firmware command (mrsas_issue_blocked_cmd, :301) β€” a very wide race window; it reads ioctl_data_mem[i] OOB from the stack and uses the value as the copyout source
  • mrsas_ioctl.c:354 the cleanup loop re-reads a third time and calls bus_dmamap_unload(ioctl_data_tag[i], ...) with OOB-read garbage

A second thread of the ioctl caller (any process with the device open β€” /dev/mrsasN is root:operator 0660, mrsas.c:790-792) flips sge_count after validation β†’ i runs toward 2^32-1 β†’ OOB write of kernel heap pointers at &ioctl_data_tag[i] with a fully attacker-chosen offset (8Β·i bytes past the array β€” i.e., across ida-style adjacent locals, saved registers and the return address of mrsas_passthru), OOB stack reads used as copyout sources (kernel stack contents disclosed when the slot holds a valid kernel pointer, copyout only validates the destination), and wild bus_dmamap_unload calls in cleanup.

Additionally, because there is no copyin, an unmapped or read-only ioctl argument makes the kernel fault on the first raw deref (mrsas_ioctl.c:172) with no on-fault protection β†’ kernel panic.

Build / run (on the DragonFly guest)

cc -O2 -Wall -o df4834 df4834_harness.c -lpthread
./df4834

Expected

  • arm 1 (race at the validationβ†’use boundary): SIGSEGV at the guard page standing in for the rest of the kernel stack frame; canary slots 16..~591 (kernel: return-address/saved-regs region) overwritten with heap pointers.
  • arm 2 (deterministic wide window; flip during the FW command): copyout loop reads ioctl_data_mem[16..23] from past the array β€” kernel stack values used as copyout sources.
  • arm 3 (FIXED model β€” kernel copy of the packet): all loops stay in [0,16), no writes past the arrays.

See run.log, VERDICT.md, fix.diff.

VERDICT.md
↓ download raw

DF-4834 β€” VERDICT

REPRODUCED (harness; the guest has no LSI MegaRAID controller β€” same methodology as DF-4559/DF-4363).

The code path (traced)

  1. mrsas_ioctl() β€” sys/dev/raid/mrsas/mrsas.c:1112-1159. arg = ap->a_data (line 1116) is the raw userland pointer of ioctl(2); line 1151 hands it to mrsas_passthru(sc, (void *)arg) with no copyin.
  2. mrsas_passthru() β€” sys/dev/raid/mrsas/mrsas_ioctl.c:148-370. user_ioc therefore points into user address space for the whole call: - :172 in_cmd->dcmd.opcode β€” first raw user deref (unmapped arg β‡’ unprotected kernel page fault β‡’ panic; on SMAP-enabled CPUs every call faults). - :174 user_ioc->frame.hdr.cmd_status = MFI_STAT_OK β€” raw kernel write into user memory (RO mapping β‡’ panic). - :186 validation user_ioc->sge_count > MAX_IOCTL_SGE (16) reads user mem. - :153-156 stack arrays ioctl_data_tag[16] / ioctl_data_dmamap[16] / ioctl_data_mem[16] / ioctl_data_phys_addr[16]. - :225-264 the copyin loop re-reads user_ioc->sge_count every iteration and writes &ioctl_data_tag[i] (:237), &ioctl_data_mem[i] (:241-242), &ioctl_data_phys_addr[i] (:246-248), plus kern_sge32[i] (:254-255 β€” into the 1024-byte frame DMA block). - :301 mrsas_issue_blocked_cmd() β€” the firmware round-trip (up to 180 s wait budget, MRSAS_INTERNAL_CMD_WAIT_TIME=180) β€” a very wide window. - :307-314 copyout loop β€” second re-read of sge_count/sgl[]; reads ioctl_data_mem[i] OOB and uses the stack value as the copyout source (copyout validates only the destination, sys/platform/pc64/x86_64/support.s:228-258 β€” a valid kernel pointer as source becomes a kernel-memory disclosure). - :354-364 cleanup loop β€” third re-read; bus_dmamap_unload(ioctl_data_tag[i], ...) with OOB-read stack garbage.
  3. The ioctl is reachable by root and the operator group (make_dev(..., UID_ROOT, GID_OPERATOR, 0660), mrsas.c:790-792).

What the harness proves (run.log)

  • arm 1 β€” flip sge_count 16β†’0x20000 right after the check: the loop writes ioctl_data_tag[i]/ioctl_data_mem[i] out of bounds until it hits the guard page (SIGSEGV). Canary slots 16..591 recorded hits β€” in the kernel those slots are the rest of mrsas_passthru's frame (adjacent locals, saved registers, return address) and the write value is a heap pointer (bus_dma_tag). Attacker-chosen offset: 8Β·i bytes, i up to 2^32-1.
  • arm 2 β€” deterministic mid-command flip: the copyout loop reads ioctl_data_mem[16..23] past the array and uses the (kernel stack) values as copyout sources β€” the leak/wild-deref class, no tight race required.
  • arm 3 β€” kernel-copy model (the fix): racer is invisible, loops stay bounded, zero writes past the arrays.

Escalation ceiling (honest)

Primitive = controlled-offset kernel stack OOB write of kernel heap pointers + OOB stack reads (disclosure when a valid pointer sits in the slot) + wild bus_dmamap_unload on garbage. On this non-SMEP/no-KASLR target a full stack-pivot chain is plausible, but the ioctl requires root/operator and the guest has no mrsas device, so end-to-end uid=0 could not be demonstrated β€” same limitation recorded for the pass-1 family (DF-1917..DF-1919, PR:L per this audit's convention).

Fix

Copy the packet in at the ioctl entry and operate on the kernel copy (fix.diff, kernel-copy model validated by harness arm 3).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Validated with the harness fixed-model arm (kernel copy of the packet at ioctl entry): racer writes to user memory are invisible to the kernel loops, all three loops stay in [0,16), zero writes past the arrays, clean exit. fix.diff not applied to the read-only sys/ tree; kernel-build validation not meaningful without an mrsas controller in the guest.

run.log arm 3: '[arm 3] FIXED model: kernel copy of the packet (copyin at entry) / loops stayed in [0,16): clean exit; writes past arrays: none'; fix.diff in pack.
↓ fix.diffper-fix-DF-4834

Confirmed kernel references

Detail

Exploit chain

open /dev/mrsasN (root/operator) -> ioctl(MRSAS_IOC_FIRMWARE_PASS_THROUGH, arg) with sge_count=16 and non-empty sgl[] -> thread2 flips arg->sge_count to N right after the kernel's :186 check (or during the multi-ms FW command for the copyout/cleanup loops) -> mrsas_ioctl.c:225-264 writes &ioctl_data_tag[i] for i>=16 at attacker-chosen 8*i offset (value = kmalloc'd bus_dma_tag pointer) -> smash of mrsas_passthru frame (saved regs / return address) -> crash-grade kernel stack corruption; alt path: copyout loop OOB stack reads -> kernel stack disclosure / wild bus_dmamap_unload.

Evidence (decisive lines)

run.log: '[arm 1] -> SIGSEGV at guard page: ioctl_data_tag[i]/ioctl_data_mem[i] written OUT OF BOUNDS, i ran toward 0x20000', 'canary: first OOB slot=16 (byte +0 past the 16-entry arrays), last=591'; arm 2 shows copyout(ioctl_data_mem[16..23] = 0xfffff000... kernel stack values); arm 3 (fixed kernel-copy model) 'writes past arrays: none'. VERDICT.md has the full path:line trace.

PoC changes

No seed PoC (pass-2 new finding). Harness written from scratch: faithful packed replica of struct mrsas_iocpacket (mrsas_ioctl.h:80-92), the four MAX_IOCTL_SGE=16 stack arrays placed below a guard page, verbatim :225-264 loop with re-reading user-memory bound (volatile stands in for the kernel's opaque extern calls), pipe-handshake racer, deterministic mid-FW-command arm, fixed kernel-copy arm. One earlier local iteration hung (uninitialized pthread join on DFly) and one had a false-positive in the fixed-arm checker; both fixed before the recorded run.

Verified recommended fix

copyin the mrsas_iocpacket to a kernel struct in mrsas_ioctl() before mrsas_passthru() and copyout the updated header after (fix.diff in pack)

Verdict

REPRODUCED (harness; guest has no mrsas controller β€” same methodology as DF-4559/DF-4363). mrsas_ioctl() (mrsas.c:1151) passes the raw userland a_data pointer into mrsas_passthru() with no copyin; the :186 sge_count<=16 validation reads USER memory and the :225 copyin loop, :307 copyout loop and :354 cleanup loop each RE-read sge_count/sgl[] from user memory. A second thread of the caller (device is root:operator 0660, mrsas.c:790) flips sge_count after validation, so i walks toward 2^32-1 and bus_dma_tag_create/bus_dmamem_alloc write kernel heap pointers at &ioctl_data_tag[i]/&ioctl_data_mem[i]/&ioctl_data_phys_addr[i] past the 16-entry stack arrays (mrsas_ioctl.c:153-156) β€” controlled-offset kernel stack OOB write across saved registers/return address; the copyout loop additionally reads ioctl_data_mem[i] OOB and uses kernel stack values as copyout sources (disclosure when valid), and cleanup calls bus_dmamap_unload with garbage tags. An unmapped/read-only ioctl arg instead faults the kernel at the first raw deref (:172/:174) with no on-fault handler. Harness: guard-page arm SIGSEGVs with canary slots 16..591 clobbered; deterministic mid-FW-command arm shows the copyout OOB reads; kernel-copy (fix) model stays bounded and clean. uid=0 chain not demonstrable without the HBA; primitive is kernel stack corruption.