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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4399 Β· 8 files
FileTypeDescriptionSize
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.md
↓ download raw

VERDICT β€” DF-4399

Status: inconclusive (hardware-gated; user-reachable path is certain from code).

Proof chain (all paths cited)

  1. User controls gcmd->size. tws_ioctl dispatches TWS_IOCTL_FIRMWARE_PASS_THROUGH to tws_passthru() (tws_user.c:73-74). tws_passthru() copies the user command packet verbatim: tws_user.c:126-127 memcpy(&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).
  2. The offset is used as a write pointer. tws_dmamap_data_load_cbfn (tws_cam.c:1030-1038): the non-EXECUTE_SCSI passthru branch computes sgl_ptr = (u_int32_t *)gcmd + gcmd->size and tws_fill_sg_list writes num_sgl_entries descriptors 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_header is 128 B; tws_command_packet is 1152 B).
  3. Arithmetic (pack(1), tws_hdm.h:287-336). tws_command_header is 128 B; tws_command_packet = 128 + 896 = 1024 B; arena dma_mem = 256Β·1024 + 512 B (tws.c:463-464). size=255 β‡’ SGL write starts at cmd_buf[2] + 128 + 4Β·255 = arena+3196 = offset 124 of cmd_buf[3]'s packet β€” exactly its header_desc (size_header/request_id u16/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 corrupted header_desc.request_id is the very field the error path trusts (tws_cam.c:512-513, DF-1590), and packet 3's header is the sense area sense_bufs[3].hdr points at (tws.c:617).
  4. 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_testable
baseline no→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-4399

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.