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

twa(4) firmware-passthrough buffer_length completely unvalidated: unbounded kernel kmalloc/copyin (memory-exhaustion DoS) and silent no-payload execution for lengths >= 0xFFFFFE01

Summary

tw_osli_fw_passthru never bounds the raw u32 buffer_length before kmalloc(M_WAITOK)+copyin+copyout. The DMA tags are capped at 128KB, so any larger value fails later at bus_dmamap_load - after the kernel already allocated and copied the whole buffer. One ioctl with 0x7FFFFFFF forces a 2GiB M_WAITOK allocation plus 2GiB copyin; the wrap window silently executes as no-payload. Upstream FreeBSD 13.0 identical. Root-gated (0600) on 3ware hardware. Arithmetic harness on the guest proves every accepted length AND REFUTES the scarier wrap-to-undersize heap-overflow reading (roundup2 with power-of-two modulus returns >=x or exactly 0). Cap-at-tag-limit fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4273 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.4 KB ↓ raw
VERDICT.md β€” 5.2 KB ↓ raw
twa_roundup2_overflow.c β€” 3.7 KB view raw
build.sh β€” 120 B view raw
run.sh β€” 107 B view raw
build.log β€” 11 B view raw
run.log β€” 2.4 KB view raw
env.txt β€” 842 B view raw
fix.diff β€” 743 B view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 3.3 KB view raw

DF-4273 β€” twa(4) passthrough buffer_length completely unvalidated at the ioctl boundary

File: sys/dev/raid/twa/tw_osl_freebsd.c (pass 2)

What this pack contains

  • twa_roundup2_overflow.c β€” userspace harness that reproduces, with the exact kernel macro (sys/sys/param.h:403) and exact kernel types (TW_UINT32 buffer_length, TW_UINT32 sg_size_factor), the allocation-vs-copy arithmetic of tw_osli_fw_passthru() for attacker-chosen buffer_length values, and probes for /dev/twa0.
  • build.sh, run.sh β€” exact commands (run on the QEMU guest).
  • build.log, run.log β€” full untrimmed output.
  • env.txt β€” guest uname, PCI inventory (no vendor 0x13C1 device), /dev/twa* absent, busdma tunables.
  • VERDICT.md β€” full analysis narrative including the false-positive kill of the "roundup2 wrap β†’ undersized buffer" theory.
  • fix.diff β€” proposed bound check.
  • manifest.json, verdict.json.

Reproduce

cc -O2 -o twa_roundup2_overflow twa_roundup2_overflow.c
./twa_roundup2_overflow

Expected: table showing kmalloc() size vs copyin()/copyout() length for each buffer_length; values up to 0x7FFFFFFF (and beyond) are accepted with no upper bound (TW_CL_MAX_IO_SIZE/128KB is never checked before the kernel allocation), and values in [0xFFFFFE01, 0xFFFFFFFF] wrap roundup2() to 0 (the command is silently treated as having no data payload).

VERDICT.md
↓ download raw

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

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.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored from line-accurate root cause; not build/boot-validated because the driver cannot attach on the audit guest (no 3ware hardware) and the bug itself was not runtime-reproduced there

findings/poc/DF-4273/fix.diff (bound check inserted before tw_osli_get_request at tw_osl_freebsd.c:912)
↓ fix.diffper-fix-DF-4273

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (arithmetic table: 0x7FFFFFFF -> kmalloc(0x80000000) unchecked; 0xFFFFFF01..0xFFFFFFFF -> kmalloc(0) no-payload path; /dev/twa0 ENOENT), env.txt (guest PCI inventory + uname), VERDICT.md (full derivation incl. the false-positive kill)

PoC changes

n/a (finding originated in this run; harness written fresh, fixed a missing include after first build)

Verified recommended fix

Reject buffer_length > TW_CL_MAX_IO_SIZE with E2BIG before any allocation in tw_osli_fw_passthru (see fix.diff)

Verdict

The validation gap is certain from code and its arithmetic was reproduced on the guest: tw_osli_fw_passthru() performs kmalloc(roundup2(buffer_length, sg_size_factor)) + copyin(buffer_length) + copyout(buffer_length) with no upper bound on the caller-supplied TW_UINT32 buffer_length (TW_CL_MAX_IO_SIZE/128KB is enforced only later, at bus_dmamap_load). A single ioctl can demand a multi-gigabyte M_WAITOK kernel allocation and copyin, and lengths >= 0xFFFFFE01 wrap roundup2() to 0, silently executing the firmware command with no data payload. The tempting heap-overflow reading (wrap producing a small nonzero allocation overrun by a huge copyin) is impossible: for power-of-two modulus y, (x + y-1) wraps into [0, y-1) and masks to exactly 0 - proven on-guest (0xFFFFFF01 -> kmalloc(0)). End-to-end impact could not be observed: /dev/twa0 is mode 0600 (root) and the QEMU guest has no 3ware 9000 HBA (vendor 0x13C1 absent from pciconf), and QEMU cannot emulate one. Upstream FreeBSD 13.0 carries the identical unbounded code.