# 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

```c
// 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.
