Passthrough SGL written at user-controlled offset: gcmd->size (u8, verbatim from TWS_IOCTL_FIRMWARE_PASS_THROUGH) used as SGL destination offset β lands on neighboring request's header_desc
Summary
The passthru copies the user packet verbatim incl. generic.size; the dmamap load callback computes sgl_ptr=(u32*)gcmd+size and writes nseg 16-byte descriptors there. size=255 lands byte-exact on the neighboring request's header_desc (the request_id the error path trusts) and command body; a 128K buffer spans into packet-3. The DMA tag caps the segment COUNT but not the offset. Root via 0600 /dev/tws0 corrupts unrelated in-flight kernel command state. Clamp-the-offset fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4399 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 3.2 KB | β raw | |
| trigger.c | β | 2.1 KB | view raw | |
| build.sh | β | 82 B | view raw | |
| run.sh | β | 134 B | view raw | |
| fix.diff | β | 1.2 KB | view raw | |
| verdict.json | β | 2.5 KB | view raw | |
| env.txt | β | 541 B | view raw |
DF-4399 β Passthrough SGL written at user-chosen offset (gcmd->size) into the shared command DMA arena
What
sys/dev/raid/tws/tws_cam.c:1029-1045 (tws_dmamap_data_load_cbfn): for a
passthrough request whose opcode is NOT TWS_FW_CMD_EXECUTE_SCSI, the scatter/gather
list destination is computed as
gcmd = &req->cmd_pkt->cmd.pkt_g.generic;
sgl_ptr = (u_int32_t *)(gcmd) + gcmd->size; /* size is u_int8_t, 0..255 */
gcmd->size += sgls * ((is64bit && !use_32bit_sgls) ? 4 : 2);
tws_fill_sg_list(sc, segs, sgl_ptr, sgls);
gcmd->size is taken verbatim from the user: tws_passthru()
(sys/dev/raid/tws/tws_user.c:126-127) copies the whole
struct tws_command_apache (including pkt_g.generic.size) from the
TWS_IOCTL_FIRMWARE_PASS_THROUGH (_IOWR('T',202,...)) ioctl buffer before
submission, and nothing validates it.
tws.c:463-464, 499-509), so this is corruption of unrelated in-flight
kernel command packets (live SCSI I/O command headers/SGRs) with
semi-controlled data (bus addresses of the attacker's buffer, page-multiple
lengths) β not a wild heap write.
Precise landing (pack(1) layout, tws_hdm.h:287-336): packet size is
1024 B (128 B header + 896 B body). size=255 β SGL starts at
cmd_buf[2] + 128 + 1020 = arena+3196, which is offset 124 of packet 3 β
byte-for-byte its header_desc (size_header, request_id u16,
size_sense, header bytes 124..128) and then its command body. With a
128 kB buffer (32 segments Γ 16 B) it spans up to packet-3 offset 636.
Impact
Root-only (/dev/tws0 is 0600 root:operator, tws.c:284-286). A privileged user
corrupts neighboring command packets including the header through which the
firmware DMA-writes sense data (sense_bufs[i].hdr, tws.c:617), so the
firmware can be fed forged request ids / SGLs of concurrent I/O β kernel-level
memory corruption reachable from a file-descriptor ioctl.
Trigger
trigger.c β root opens /dev/tws0, issues TWS_IOCTL_FIRMWARE_PASS_THROUGH
with opcode TWS_FW_CMD_SET_PARAM (0x13), generic.size = 255,
buffer_length = 4096. The callback then writes the SGL at gcmd+1020.
Reproduce
cc -O2 -o trigger trigger.c # build sudo ./trigger /dev/tws0 # run on a host with a tws(4) controller
Expected: kernel log noise from corrupted neighbor commands / unstable I/O; on a debug kernel, tws trace entries show request 3 misbehaving.
Status on the audit QEMU guest
No 3ware HBA present β inconclusive; offset arithmetic proven from struct sizes
(tws_hdm.h:273-336: header 128 B, packet 1152 B, size u8) in VERDICT.md.
VERDICT β DF-4399
Status: inconclusive (hardware-gated; user-reachable path is certain from code).
Proof chain (all paths cited)
- User controls
gcmd->size.tws_ioctldispatchesTWS_IOCTL_FIRMWARE_PASS_THROUGHtotws_passthru()(tws_user.c:73-74).tws_passthru()copies the user command packet verbatim:tws_user.c:126-127memcpy(&req->cmd_pkt->cmd, &ubuf->cmd_pkt.cmd, sizeof(struct tws_command_apache));βpkt_g.generic.size(tws_hdm.h:273-281,u_int8_t) is included, no validation anywhere between copy and submission (tws_user.c:140). - The offset is used as a write pointer.
tws_dmamap_data_load_cbfn(tws_cam.c:1030-1038): the non-EXECUTE_SCSI passthru branch computessgl_ptr = (u_int32_t *)gcmd + gcmd->sizeandtws_fill_sg_listwritesnum_sgl_entriesdescriptors there (16 B each in 64-bit SGL mode,tws_cam.c:1059-1073).gcmd=cmd_buf[2].cmd+ 128 B header (tws_hdm.h:287-336:tws_command_headeris 128 B;tws_command_packetis 1152 B). - Arithmetic (pack(1),
tws_hdm.h:287-336).tws_command_headeris 128 B;tws_command_packet= 128 + 896 = 1024 B; arenadma_mem= 256Β·1024 + 512 B (tws.c:463-464).size=255β SGL write starts atcmd_buf[2] + 128 + 4Β·255 = arena+3196= offset 124 of cmd_buf[3]'s packet β exactly itsheader_desc(size_header/request_idu16/size_sense, header bytes 124..128) β then its command body; a 128 kB buffer (32 segs Γ 16 B) spans to packet-3 offset 636. The corruptedheader_desc.request_idis the very field the error path trusts (tws_cam.c:512-513, DF-1590), and packet 3's header is the sense areasense_bufs[3].hdrpoints at (tws.c:617). - Bounds that do NOT help: the data DMA tag caps nseg at 46/93
(
tws.c:460-462, 518-530) β caps entry COUNT, not the offset. The arena (dma_mem,tws.c:463-464) is 256Β·1152+512 B so the write stays inside the allocation (no allocator metadata damage β the victim is other requests' live command state).
Why severity Low
The ioctl node is make_dev(..., UID_ROOT, GID_OPERATOR, S_IRUSR|S_IWUSR, "tws%u")
(tws.c:284-286) β root-only. Within the root threat model it is still a genuine
kernel-memory-safety defect (CWE-787) and corrupts other requests' state, which is
not what even a root caller of a single passthrough command expects to be allowed.
Trigger verification
Not possible on the audit guest: no vendor 0x13c1 device, /dev/tws* absent,
tws not loaded (env.txt). On real hardware sudo ./trigger /dev/tws0 issues the
corrupting ioctl.
Fix validation
fix.diff clamps gcmd->size so the SGL can never start beyond the request's own
giga-packet body (and never overlaps the following packet). Not testable without
the controller (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 bounds the SGL destination to the request's own giga packet; cannot be validated on the audit guest (no 3ware controller).
fix.diff (guard in tws_dmamap_data_load_cbfn)
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md proof chain; trigger.c (real _IOWR('T',202) ioctl, size=255, SET_PARAM opcode); fix.diff (sink-side clamp); env.txt
PoC changes
no seed; trigger written from scratch using the real _IOWR('T',202,struct tws_ioctl_no_data_buf) definition and pack(1) mirror structs
Verified recommended fix
Clamp gcmd->size against sizeof(union tws_command_giga)/4 minus the SGL extent before computing sgl_ptr in tws_dmamap_data_load_cbfn (and reject rather than submit oversized offsets).
Verdict
Code-level proof: the passthrough ioctl copies the user's command packet verbatim (tws_user.c:126-127) including pkt_g.generic.size (u_int8_t, tws_hdm.h:275); tws_dmamap_data_load_cbfn (tws_cam.c:1034-1038) uses that byte as the u32-unit offset for the SGL write, so size=255 puts 16-byte DMA descriptors at cmd_buf[2]+1148 = offset 124 of cmd_buf[3]'s packet β precisely its header_desc (size_header/request_id/size_sense) and command body β corrupting unrelated in-flight kernel command state. The DMA tag caps segment COUNT (46/93, tws.c:518-530) but not the offset. Root-only (/dev/tws0 is 0600, tws.c:284-286). Guest has no 3ware HBA, so not exercisable there.
No comments yet.