# DF-4273 VERDICT — unvalidated passthrough buffer_length (input-validation gap; wrap-overflow theory refuted)

## Bottom line

`tw_osli_fw_passthru()` (`sys/dev/raid/twa/tw_osl_freebsd.c:894-1051`) never
validates `user_buf->driver_pkt.buffer_length` (a raw `TW_UINT32` from the
caller's ioctl buffer, `tw_cl_ioctl.h:96`) before computing

```c
data_buf_size_adjusted = roundup2(user_buf->driver_pkt.buffer_length, sc->sg_size_factor);  /* :927 */
req->data = kmalloc(data_buf_size_adjusted, TW_OSLI_MALLOC_CLASS, M_WAITOK);                /* :930 */
copyin(user_buf->pdata, req->data, user_buf->driver_pkt.buffer_length);                     /* :933 */
```

The ioctl DMA tag is capped at `TW_CL_MAX_IO_SIZE` (0x20000 = 128 KB,
`tw_cl_share.h:72`, tags built at :599-639), so any `buffer_length` above 128 KB
can *never* be mapped and will fail later at `bus_dmamap_load()` with EFBIG —
but only **after** the kernel has already allocated and `copyin`'d the entire
buffer. A single ioctl with `buffer_length = 0x7FFFFFFF` makes the kernel try
to `kmalloc(2 GiB, M_WAITOK)` and then `copyin` 2 GiB, pinning that memory and
blocking the ioctl thread; near-2^32 values (`>= 0xFFFFFE01`) wrap
`roundup2()` to 0 and silently execute the command as a *no-payload* command
(the driver tells firmware "no data" while userspace believes it sent data).
Upstream FreeBSD 13.0 has the identical unbounded code (verified against
release/13.0.0 `sys/dev/twa/tw_osl_freebsd.c`) — this is a shared validation
gap, not a DragonFly divergence.

## The false-positive kill (important for future auditors)

The obvious "critical" reading of this code — that 32-bit wrap in
`roundup2(buffer_length, 512)` could produce a *small nonzero* rounded size
followed by a multi-GiB `copyin` into it (kernel heap overflow) — is **wrong**:

`roundup2(x, y) = ((x + (y-1)) & ~(y-1))` with both operands `uint32_t`. For
any power-of-two `y`, the addition only wraps when `x >= 2^32 - (y-1)`, and in
that window the pre-mask sum is in `[0, y-1)`, so the mask always yields
**exactly 0** — never a small nonzero value. `roundup2` can only return
`>= x` or `0`. When it returns 0 the driver takes the no-payload branch
(`if ((req->length = data_buf_size_adjusted))` at :929 is false) and no
`copyin`/`copyout` happens at all. **There is no size-mismatch memory
corruption on this path.** This was demonstrated empirically on the guest
(`run.log`: `buffer_length=0xFFFFFF01 -> kmalloc(0x0=0) ... no-payload path
taken`), not just by inspection.

What remains is therefore the honest, smaller impact:
1. **Unbounded kernel allocation + unbounded copyin/copyout** driven by one
   ioctl value (kernel memory exhaustion / long-pinned memory / unkillable
   60 s-blocked ioctl thread) — a robustness/DoS gap.
2. **Silent no-payload execution** for lengths in `[0xFFFFFE01, 0xFFFFFFFF]`
   (behavioral correctness: firmware command runs without its data buffer).

## Reachability

- `/dev/twaN` is created `UID_ROOT, GID_OPERATOR, S_IRUSR|S_IWUSR`
  (`tw_osl_freebsd.c:405-407`) → mode 0600, root only. Not an escalation
  vector; this is a root-triggerable robustness defect on a machine that has a
  3ware 9000-series HBA (vendor 0x13C1, devices 0x1002/0x1003/0x1004/0x1005,
  `tw_cl_init.c:64-72`).
- The QEMU audit guest has no such hardware (see `env.txt`: PCI inventory is
  i440fx + virtio only, `/dev/twa*` absent), so the ioctl could not be
  exercised end-to-end there.

## Verification narrative

1. Read the whole file; identified the roundup2/copyin pair as the only
   user-length-driven kernel copies in the file (the CL ioctl AEN path
   `tw_cl_ioctl()` copies fixed `sizeof(struct tw_cl_event_packet)` chunks —
   no user length; and its modulo index is unsigned-only, so no negative OOB).
2. Built the arithmetic harness on the guest (`BUILD_OK`, `build.log`), ran it
   as unprivileged user `maxx` (`run.log`): confirmed every `buffer_length`
   up to `0xFFFFFC01` is accepted for allocation with no bound check, and the
   near-2^32 window maps to `kmalloc(0)` (no-payload path) — killing the
   heap-overflow theory and confirming the no-validation gap in one run.
3. Confirmed on-guest absence of the target hardware (`env.txt`).

Status: **inconclusive** (arithmetic/behavior reproduced on guest; end-to-end
ioctl impact not observable — no 3ware controller exists in the guest and
QEMU does not emulate one). Severity filed: **Low** (root-only device + rare
hardware; no memory corruption proved possible).

## Fix

`fix.diff` — reject `buffer_length > TW_CL_MAX_IO_SIZE` up front with `E2BIG`
before any allocation (mirrors the DMA-tag cap, also fixes the silent
no-payload wrap behavior by rejecting rather than truncating). Fix not
build-validated (no target hardware to regression-test against; kernel build
for a driver that cannot attach on this guest would prove nothing about the
bug itself) → `fix_status: not_testable`.

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