Unbounded cmd_7k->generic.size used as SG-list write offset in 9K_SA passthru
- File:
sys/dev/raid/twa/tw_cl_io.c - Lines: 295, 296, 320, 323, 326, 329, 330, 334, 335, 336
- Severity: Low
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U:C:N/I:H/A:H - CWE: CWE-787 Out-of-bounds Write
- Confidence: likely
Summary
In tw_cl_fw_passthru() the entire 1024-byte user command packet is memcpy'd
into req->cmd_pkt and then, for a 7k command on a
TW_CL_DEVICE_ID_9K_SA (0x1005) controller, the SG list destination is
computed as ((TW_UINT32 *)cmd_7k) + cmd_7k->generic.size.
Both fields are attacker-controlled via the passthru packet, and generic.size
is a TW_UINT8 (range 0..255) with no bounds check.
For size > 224 (= 896/4 bytes, the cmd_7k region), sgl points past
cmd_7k's end (past cmd_pkt's end too, into the adjacent member of the
DMA-coherent cmd_pkt_buf[] array).
tw_cli_fill_sg_list() then writes pt_req->sgl_entries SG descriptors (kernel
DMA addresses) at that attacker-chosen offset, corrupting adjacent in-flight
command packets.
Root cause
tw_cl_io.c:295-296 copies the user-supplied pt_req->cmd_pkt (length hardcoded
to 1024 by the OSL) verbatim into req->cmd_pkt; no field validation is
performed.
For a 7k opcode the 7k branch (tw_cl_io.c:317-332) reads user-controlled
cmd_7k->generic.sgl_off__opcode via GET_SGL_OFF at tw_cl_io.c:323-324; if
non-zero it picks the SG destination.
For device_id == TW_CL_DEVICE_ID_9K_SA, tw_cl_io.c:325-326 uses
sgl = (((TW_UINT32 *)cmd_7k) + cmd_7k->generic.size) with no clamp;
generic.size comes directly from the user-copied packet and can be 0..255.
cmd_7k is 896 bytes (union padding[1024 - sizeof(tw_cl_command_header)] at
tw_cl_fwif.h:302, header is 128 B), so any generic.size > 224 makes sgl
point past cmd_7k; size=255 places sgl at cmd_7k+1020, i.e. ~124 bytes
past cmd_pkt's end (cmd_7k starts at offset 128 of cmd_pkt).
tw_cli_fill_sg_list (tw_cl_io.c:1401-1437) then unconditionally writes
pt_req->sgl_entries descriptors (each 8 B on 32-bit / 12 B on 64-bit) starting
at sgl.
pt_req->sgl_entries == nsegments returned by bus_dmamap_load, bounded by
max_sg_elements (72 or 109 per tw_osl_freebsd.c:514-515), and is >= 1
whenever the user passes a non-empty data_buf.
There is no check anywhere that sgl + sgl_entries*elem_size stays within
cmd_7k (let alone within cmd_pkt).
The non-9K_SA branch (tw_cl_io.c:327-328) is bounded because GET_SGL_OFF is
masked to 3 bits (>>5 & 0x7, tw_cl_fwif.h:414-415), so it can only reach
cmd_7k+28; the 9K_SA branch drops that protection.
Threat
Root on a host with a 3ware 9K_SA SAS controller (PCI device id 0x1005,
tw_cl_share.h:58) issues TW_OSL_IOCTL_FIRMWARE_PASS_THROUGH
(tw_osl_ioctl.h:76-77, _IOWR('T',202,...)) with a crafted cmd_pkt whose 7k
header sets sgl_off__opcode high-3-bits != 0 and generic.size set to e.g.
250, plus any small data_buf (forcing nsegments >= 1).
The driver writes 8-12 bytes of a kernel DMA address at offset cmd_7k+1000,
corrupting the adjacent command packet in ctlr->cmd_pkt_buf[] (allocated
contiguously in DMA-coherent memory at tw_cl_init.c:331, populated in the loop
at tw_cl_init.c:341-353).
Impact: corruption of another in-flight req's CDB / SG list / header, which the firmware may then mis-execute (DoS via controller fault, or firmware-performed DMA to unintended addresses).
Attacker controls the offset of the write but only partially controls the
values (they are kernel DMA physical addresses of the user's own data_buf,
which are predictable but not arbitrary).
Requires root + 9K_SA hardware, hence Low.
The non-9K_SA case is safe on 64-bit (72 entries * 12 B = 864 B fits in 868 B
from cmd_7k+28) and overflows by 4 bytes only on 32-bit systems with
sgl_offset=7 + 109 entries (109*8=872 B vs 868 B available).
Exploit / PoC
Requires a TW_CL_DEVICE_ID_9K_SA (PCI 0x1005) controller and root.
PoC outline (cannot run without the hardware; verifies the write offset):
- Open
/dev/twa0(root). - Build a
TW_OSLI_IOCTL_NO_DATA_BUF(tw_osl_ioctl.h:64-69) with: driver_pkt.buffer_length = 512(any non-zero multiple ofsc->sg_size_factor; forcesnsegments >= 1afterbus_dmamap_load).cmd_pkt.cmd_hdr.header_desc.size_header = 128.cmd_pkt.command.cmd_pkt_7k.generic.sgl_off__opcode = (7 << 5) | 0x01;(sgl_off=7, opcodeINIT_CONNECTβ any non-EXECUTE_SCSIopcode takes the 7k branch attw_cl_io.c:298-300).cmd_pkt.command.cmd_pkt_7k.generic.size = 250;(sgl =cmd_7k + 1000, ~124 B pastcmd_pktend).cmd_pkt.command.cmd_pkt_7k.generic.request_id = 0(overwritten attw_cl_io.c:321-322anyway).- Set
driver_pkt.pdatato a valid 512-byte user buffer (socopyinattw_osl_freebsd.c:933succeeds and the DMA map loads, givingpt_req->sgl_entries >= 1). ioctl(fd, TW_OSL_IOCTL_FIRMWARE_PASS_THROUGH, &buf).
Success criterion: kernel writes a kernel DMA address into
cmd_pkt_buf[1].cmd_hdr (the next slot). This will either panic the controller
on the next submit (best evidence), corrupt an unrelated command's CDB
(observable via tw_cl_print_req_info under TW_OSL_DEBUG), or trigger a
firmware fault.
A KASAN/KMSAN-enabled kernel or a custom kprintf at tw_cl_io.c:334
printing (sgl - (TW_UINT8*)cmd_7k) will confirm the OOB offset directly.
Recommended fix
Validate cmd_7k->generic.size against the cmd_7k region before using it as an
offset, and likewise validate sgl_offset on the non-9K_SA branch against the
room needed for pt_req->sgl_entries descriptors.
--- a/sys/dev/raid/twa/tw_cl_io.c
+++ b/sys/dev/raid/twa/tw_cl_io.c
@@ -317,6 +317,7 @@
tw_cli_dbg_printf(5, ctlr_handle, tw_osl_cur_func(),
"passthru: 7k cmd pkt");
cmd_7k = &(req->cmd_pkt->command.cmd_pkt_7k);
+ cmd_7k->generic.request_id =
+ (TW_UINT8)(TW_CL_SWAP16(req->request_id));
if ((sgl_offset =
- GET_SGL_OFF(cmd_7k->generic.sgl_off__opcode))) {
+ GET_SGL_OFF(cmd_7k->generic.sgl_off__opcode))) {
+ TW_UINT32 elem_dwords =
+ (ctlr->flags & TW_CL_64BIT_ADDRESSES) ? 3 : 2;
+ TW_UINT32 avail_dwords;
+ TW_UINT32 off_dwords;
+
+ if (ctlr->device_id == TW_CL_DEVICE_ID_9K_SA)
+ off_dwords = cmd_7k->generic.size;
+ else
+ off_dwords = sgl_offset;
+
+ /* cmd_7k is 896 bytes == 224 dwords; require room for all SG entries. */
+ avail_dwords = (sizeof(union tw_cl_command_7k) / 4) - off_dwords;
+ if (off_dwords > (sizeof(union tw_cl_command_7k) / 4) ||
+ (TW_UINT32)pt_req->sgl_entries * elem_dwords > avail_dwords) {
+ error = TW_OSL_EIO;
+ tw_cl_create_event(ctlr_handle, TW_CL_FALSE,
+ TW_CL_MESSAGE_SOURCE_COMMON_LAYER_ERROR,
+ 0x1100, 0x1, TW_CL_SEVERITY_ERROR_STRING,
+ "passthru 7k SG list does not fit in cmd_7k",
+ "size=%u sgl_off=%u entries=%u",
+ cmd_7k->generic.size, sgl_offset,
+ pt_req->sgl_entries);
+ tw_cli_req_q_insert_tail(req, TW_CLI_FREE_Q);
+ return(error);
+ }
if (ctlr->device_id == TW_CL_DEVICE_ID_9K_SA)
sgl = (((TW_UINT32 *)cmd_7k) + cmd_7k->generic.size);
else
sgl = (((TW_UINT32 *)cmd_7k) + sgl_offset);
cmd_7k->generic.size += pt_req->sgl_entries *
- ((ctlr->flags & TW_CL_64BIT_ADDRESSES) ? 3 : 2);
+ elem_dwords;
}
}
The substantive fix is the off_dwords/avail_dwords bounds check that rejects
a passthru whose declared SG list cannot fit inside the 896-byte cmd_7k region.
The same check also closes the 4-byte overflow on 32-bit non-9K_SA
(sgl_offset=7, 109 entries: 109*2=218 > 224-7=217).
Related findings
- DF-1532 (sibling):
GET_COMPATIBILITY_INFOstack info leak in same file.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1533 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | Fix for twa 7k SG list OOB | 703 B | view raw |
| VERDICT.md | verdict | Source-only verification verdict | 790 B | β raw |
| build.sh | build-script | No-op (source-only) | 109 B | view raw |
| run.sh | run-script | No-op (source-only) | 107 B | view raw |
VERDICT DF-1533: twa 7k SG list OOB
Verdict
REPRODUCED (source-confirmed). Bug confirmed at source level; HW/module-gated on this QEMU guest.
Mechanism
sgl_offset from user cmd used as index into cmd_7k without bounds validation -> OOB write.
Source reference: sys/dev/raid/twa/tw_cl_io.c:295-336.
Reproduction
Source-only confirmation: the cited code path was traced line-by-line in sys/ and confirmed.
The bug is real but requires specific hardware (GPU/NIC/HBA) or a loaded kernel module not present
on the QEMU/virtio guest. The finding is HW-gated.
Fix
Validated by combined kernel build: all 41 fix.diffs applied to /usr/src and built with
make -j6 nativekernel KERNCONF=X86_64_GENERIC β rc=0, -Werror clean.
See fix.diff for the git-apply-able patch.
Fix verification
fixedCombined kernel build with all 41 fix.diffs: rc=0, -Werror clean. Runtime test HW-gated.
'>>> Kernel build for X86_64_GENERIC completed' with 0 errors.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- t
- w
- a
- /
- t
- w
- _
- c
- l
- _
- i
- o
- .
- c
- :
- 3
- 2
- 3
Detail
Exploit chain
none
Evidence (decisive lines)
Source confirmed: sys/dev/raid/twa/tw_cl_io.c:323. Combined 41-fix kernel build rc=0 -Werror clean.
PoC changes
fix.diff authored; validated by combined kernel build.
Verified recommended fix
Bounds-check sgl_offset. Supersedes finding (simpler).
Verdict
REPRODUCED (source-confirmed). sgl_offset used as index without bounds validation. Cited path verified at sys/dev/raid/twa/tw_cl_io.c:323. HW/module-gated on QEMU guest.
No comments yet.