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)
PoC verification
Evidence pack
findings/poc/DF-1919 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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/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. 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.
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 fromfix.diff(mrsas_ioctl.c:266-267+bus_size_twidening + copyin usesioctl_data_size) and shows it rejects the 5 truncation vectors. - Added
fix.diff: (1) widensioctl_data_sizefrominttobus_size_t; (2) addsMRSAS_IOCTL_MAX_DATA_SIZEcap (1 MiB), rejectingiov_lenvalues above it before the truncation can take effect; (3) changes the SGElengthandcopyinlength to useioctl_data_sizeinstead of the rawiov_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 nowREJECTED (EINVAL).
fix_status: fixed β patched predicate closes the truncation vectors;
patched kernel is bootable and stable.
Fix verification
fixedVALIDATED. Combined kernel rc=0; fixcheck rejects 5/6 truncation vectors.
baseline 3 multi-GiB overflows; patched 5/6 rejected.
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
- :
- 1
- 6
- 1
- 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
- 8
- 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
- 4
- 1
- 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
- 5
- 9
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.
No comments yet.