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)
PoC verification
Evidence pack
findings/poc/DF-4273 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 oftw_osli_fw_passthru()for attacker-chosenbuffer_lengthvalues, 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).
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/twaNis createdUID_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
- 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 fixedsizeof(struct tw_cl_event_packet)chunks β no user length; and its modulo index is unsigned-only, so no negative OOB). - Built the arithmetic harness on the guest (
BUILD_OK,build.log), ran it as unprivileged usermaxx(run.log): confirmed everybuffer_lengthup to0xFFFFFC01is accepted for allocation with no bound check, and the near-2^32 window maps tokmalloc(0)(no-payload path) β killing the heap-overflow theory and confirming the no-validation gap in one run. - 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_testablefix.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)
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
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.
No comments yet.