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

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 of sc->sg_size_factor; forces nsegments >= 1 after bus_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, opcode INIT_CONNECT β€” any non-EXECUTE_SCSI opcode takes the 7k branch at tw_cl_io.c:298-300).
  • cmd_pkt.command.cmd_pkt_7k.generic.size = 250; (sgl = cmd_7k + 1000, ~124 B past cmd_pkt end).
  • cmd_pkt.command.cmd_pkt_7k.generic.request_id = 0 (overwritten at tw_cl_io.c:321-322 anyway).
  • Set driver_pkt.pdata to a valid 512-byte user buffer (so copyin at tw_osl_freebsd.c:933 succeeds and the DMA map loads, giving pt_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.

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).

  • DF-1532 (sibling): GET_COMPATIBILITY_INFO stack info leak in same file.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1533 Β· 4 files
FileTypeDescriptionSize
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.md verdict Source-only verification verdict
↓ download 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Combined 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 master DEV (41 fix.diffs applied)

Confirmed kernel references

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.