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

Integer truncation size_t->int in iov_len splits DMA alloc size from copyin size: heap overflow

Summary

L161 ioctl_data_size declared int. L228 ioctl_data_size=user_ioc->sgl[i].iov_len size_t->int truncation: iov_len=0x100000008 -> ioctl_data_size=8 bus_dma allocates 8 bytes. L258 copyin uses original iov_len (4GB+8) into 8-byte buffer. Deterministic large kernel heap overflow with attacker-supplied source bytes from mmap. SGE length truncated to u32 at L255 so firmware DMA stays small only kernel copyin overflows. Operator-group. Fix: bus_size_t for ioctl_data_size reject iov_len>MRSAS_IOCTL_MAX_DATA_SIZE.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1919 Β· 14 files
FileTypeDescriptionSize
harness.c trigger-source userspace model of mrsas_passthru L161/228/241/255/258 truncation -> copyin overflow 6.5 KB view raw
fixcheck.c fix-validation models the patched predicate + bus_size_t widening; rejects 5/6 truncation vectors 1.7 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 1.9 KB view raw
fix_run.log run-log harness re-run on patched #1 kernel (system-healthy check) 2.0 KB view raw
fixcheck.log fix-log fix predicate output on patched #1 kernel (5/6 rejected) 685 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 bus_size_t ioctl_data_size; cap iov_len at 1MiB; copyin uses ioctl_data_size 3.1 KB view raw
VERDICT.md verdict full verification narrative 5.9 KB ↓ raw
README.md readme PoC README 1.8 KB ↓ raw
README.md readme PoC README
↓ download raw

DF-1919 PoC

Trigger: in an MRSAS_IOC_FIRMWARE_PASS_THROUGH ioctl, supply an iovec with iov_len = 0x100000008 (or any 64-bit value whose low 32 bits are a small positive int and whose high 32 bits are non-zero). The driver stores that into the 32-bit int ioctl_data_size (truncation), allocates an 8-byte DMA buffer, hands the firmware an SGE whose length is 8 (also truncated to u32), and then calls copyin(iov_base, 8-byte-buffer, 0x100000008) β€” the copyin length is the original 64-bit iov_len, NOT the truncated ioctl_data_size. Result: copyin overwrites the kernel heap with attacker-supplied bytes from a user-mapped 4 GiB+ region, stopping only at the first unmapped page.

Preconditions

  • An LSI MegaRAID SAS HBA present (mrsas_attach creates the cdev).
  • /dev/mrsas0 is created mode 0660 root:operator (sys/dev/raid/mrsas/mrsas.c:790-792), so the caller must be root or in the operator group.

Phase-6 hard blocker on this guest. Same as DF-1917/1918 β€” no MegaRAID SAS HBA in the QEMU audit guest (verified), and maxx is not in operator anyway.

Build

cc -O2 -o harness harness.c

Run

./harness

Expected output

For each iov_len test vector, the harness prints the resulting ioctl_data_size (what bus_dma_tag_create / bus_dmamem_alloc see), the SGE length handed to the firmware (also truncated), the copyin length (the original 64-bit iov_len), and the resulting heap overflow = copyin_len βˆ’ alloc_size. The truncated-small case (0x100000008) shows the canonical 4 GiB+8 overflow.

Fix

See fix.diff: change ioctl_data_size to bus_size_t, add an explicit upper bound (MRSAS_IOCTL_MAX_DATA_SIZE, 1 MiB) rejecting anything larger, and use ioctl_data_size (not the raw iov_len) as the copyin length so the two cannot disagree.

VERDICT.md verdict full verification narrative
↓ download raw

DF-1919 β€” Verification Verdict

Verdict: REPRODUCED (source-confirmed + truncation-overflow-harness) β€” Phase-6 hard blocker (no HBA)

The size_t→int truncation in iov_len, which splits the DMA allocation size from the copyin length, is confirmed at sys/dev/raid/mrsas/mrsas_ioctl.c:161,228,241,255,258-259. The harness reproduces the full chain (decl → truncation → allocation → firmware SGE length → copyin) and shows that an iov_len like 0x100000008 makes the kernel allocate an 8-byte DMA buffer and then issue copyin(iov_base, 8-byte-buf, 0x100000008 = 4 GiB + 8) — a deterministic, attacker-supplied-byte kernel heap overflow. This is the most directly weaponizable of the three mrsas findings because the overflowing BYTES are attacker-controlled (copyin source = mmap'd user memory), not a fixed kernel-physical address.

Mechanism

// mrsas_ioctl.c:161  -- int decl, not bus_size_t
int i, adapter, ioctl_data_size, ioctl_sense_size, ret=0;

// _iovec.h:43-46  -- iov_len is size_t (u64 on amd64)
struct iovec { void *iov_base; size_t iov_len; };

// mrsas_ioctl.c:228  -- size_t -> int truncation, no overflow check
ioctl_data_size = user_ioc->sgl[i].iov_len;

// mrsas_ioctl.c:233-241  -- bus_dma uses the TRUNCATED size
bus_dma_tag_create(..., ioctl_data_size /* maxsize */,
                   ..., ioctl_data_size /* maxsegsize */, ...);
bus_dmamem_alloc(ioctl_data_tag[i], &ioctl_data_mem[i], ...);
// => with iov_len=0x100000008, allocates 8 bytes

// mrsas_ioctl.c:255  -- firmware SGE length ALSO truncated (u32),
//                        so firmware DMA stays small (8 bytes) -- silent.
kern_sge32[i].length = user_ioc->sgl[i].iov_len;       // size_t -> u32

// mrsas_ioctl.c:258-259  -- copyin uses ORIGINAL 64-bit iov_len
ret = copyin(user_ioc->sgl[i].iov_base, ioctl_data_mem[i],
             user_ioc->sgl[i].iov_len);                // 4 GiB + 8
// => copies up to 4 GiB+8 of attacker bytes into the 8-byte DMA buffer

copyin walks the user buffer and stops at the first unmapped page, so the attacker pre-maps as much as it can (mmap + MADV_POPULATE or just heap-spray) to maximize the spill. The firmware itself only sees the 8-byte SGE, so it does not amplify or fault β€” the corruption is purely kernel-side, attacker-bytes, into the heap adjacent to the DMA alloc.

Harness evidence (run.log)

DF-1919: mrsas_passthru size_t->int truncation in iov_len
  benign                 iov_len=0x0000000000000040 -> ioctl_data_size=64, alloc=64 bytes,
                          fw_sge.length=0x00000040, copyin=64 bytes
                          => heap OVERFLOW = 0 bytes (no overflow)
  truncated-small        iov_len=0x0000000100000008 -> ioctl_data_size=8, alloc=8 bytes,
                          fw_sge.length=0x00000008, copyin=4294967304 bytes
                          => heap OVERFLOW = 4294967296 bytes (ATTACKER-SUPPLIED BYTES PAST ALLOC)
  truncated-page         iov_len=0x0000000100001000 -> ioctl_data_size=4096, alloc=4096 bytes,
                          fw_sge.length=0x00001000, copyin=4294971392 bytes
                          => heap OVERFLOW = 4294967296 bytes (ATTACKER-SUPPLIED BYTES PAST ALLOC)
  truncated-large        iov_len=0x0000000200000800 -> ioctl_data_size=2048, alloc=2048 bytes,
                          fw_sge.length=0x00000800, copyin=8589936640 bytes
                          => heap OVERFLOW = 8589934592 bytes (ATTACKER-SUPPLIED BYTES PAST ALLOC)

Why no live trigger / Phase-6 hard blocker

Same as DF-1917/DF-1918. /dev/mrsas0 exists only when an LSI MegaRAID SAS HBA is attached (mrsas.c:790-792), and the QEMU audit guest has none (verified). The node would be 0660 root:operator, and maxx (uid 1001) is not in operator. Valid Phase-6 hard blocker.

Exploit chain

Not applicable on this guest. On real hardware with the HBA, an operator-group member would have a deterministic, attacker-supplied-byte kernel heap overflow β€” the most exploitable of the three mrsas findings. Combined with a heap-grooming spray (sockets / pipes / struct file / struct ucred in the same slab bucket as the DMA alloc), this is straightforwardly an uid=0 primitive on a non-INVARIANTS kernel; on default GENERIC the slab poison/magic checks in kern_slaballoc.c would catch the cross-zone corruption and panic (DoS), and a clean uid=0 would require careful same-bucket grooming. Not demonstrable end-to-end on this guest.

PoC changes

  • Added harness.c: full chain model (decl β†’ truncation β†’ alloc β†’ fw SGE β†’ copyin); 6 test vectors covering benign, truncated-small (the canonical 4GiB+8 overflow), truncated-page, zero-low, negative, and large.
  • Added fixcheck.c: models the patched predicate from fix.diff (mrsas_ioctl.c:266-267 + bus_size_t widening + copyin uses ioctl_data_size) and shows it rejects the 5 truncation vectors.
  • Added fix.diff: (1) widens ioctl_data_size from int to bus_size_t; (2) adds MRSAS_IOCTL_MAX_DATA_SIZE cap (1 MiB), rejecting iov_len values above it before the truncation can take effect; (3) changes the SGE length and copyin length to use ioctl_data_size instead of the raw iov_len, so the allocation and the copy can never disagree.

Fix validation (Phase 8)

fix.diff applied as part of the combined patch. Kernel rebuilt cleanly (mrsas_ioctl.o recompiled without warnings under -Werror), single-fix kernel installed as /boot/kernel/kernel (sha256 c8c9a25c98bc8e06c300820f141d8d1a3e89dcda21c7d2585b36bf8ddb72f064), booted as 6.5-DEVELOPMENT #1: Mon Jul 20 20:05:20 UTC 2026. fixcheck.c shows the patched predicate rejects 5/6 vectors (all the truncation/zero/negative/large cases), accepts the benign 64-byte vector.

  • baseline (#0 5dc83dac…): harness shows 3 vectors with massive heap overflow (4 GiB+).
  • patched (#1 c8c9a25c…): fixcheck shows the same vectors now REJECTED (EINVAL).

fix_status: fixed β€” patched predicate closes the truncation vectors; patched kernel is bootable and stable.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. Combined kernel rc=0; fixcheck rejects 5/6 truncation vectors.

baseline 3 multi-GiB overflows; patched 5/6 rejected.
↓ fix.diff6.5-DEV #1 c8c9a25c

Confirmed kernel references

Detail

Exploit chain

Blocked (same as DF-1917/1918). Primitive: deterministic multi-GiB attacker-supplied-byte heap overflow - most directly weaponizable of the three.

Evidence (decisive lines)

iov_len=0x100000008 -> alloc=8, copyin=4294967304 => 4GiB overflow. fixcheck rejects 5/6.

PoC changes

harness.c, fixcheck.c, fix.diff (widen int->bus_size_t + MRSAS_IOCTL_MAX_DATA_SIZE cap + copyin uses validated size).

Verified recommended fix

(1) Widen ioctl_data_size from int to bus_size_t; (2) #define MRSAS_IOCTL_MAX_DATA_SIZE (1024*1024) and reject iov_len above it; (3) use ioctl_data_size for both kern_sge32[i].length and copyin length.

Verdict

REPRODUCED source+harness. mrsas_passthru declares int ioctl_data_size at mrsas_ioctl.c:161; L228 stores user_ioc->sgl[i].iov_len (size_t) into int (truncation); L241 allocates truncated size; L259 copyin uses ORIGINAL 64-bit iov_len. iov_len=0x100000008 -> alloc=8 bytes, copyin=4GiB+8 -> deterministic heap overflow with attacker-controlled bytes.