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

twa: unbounded kvsprintf+strcpy into 98-byte parameter_data -> stack/heap overflow + format-string

Field Value
ID DF-1664
File sys/dev/raid/twa/tw_cl_misc.c
Lines 505, 521, 533, 538, 540, 683, 746, 757, 777, 800
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE CWE-787 Out-of-bounds Write; CWE-134 Format String
Confidence certain
Status new
CVE match variant (3ware driver event formatting class β€” similar to historical twa/tw_cli CVEs in class)
Created 2026-07-18

Summary

tw_cl_create_event formats the caller-supplied event_specific_desc (a printf format string) into event->parameter_data with tw_osl_vsprintf (kvsprintf, unbounded), then appends event_desc with tw_osl_strcpy (strcpy, unbounded). The destination parameter_data is only 98 bytes (tw_cl_share.h:205), and event_src/severity_str follow it inside the 136-byte tw_cl_event_packet struct.

Multiple in-file callers expand to > 98 bytes (boot-time controller-info string, PCI parity/abort/queue-error status strings using tw_cli_describe_bits on the full status_reg), and the firmware-controlled macro caller tw_cli_create_ctlr_event passes cmd_hdr->err_specific_desc as the format string.

Result is a stack overflow when queue_event=FALSE (event points to a stack-local event_pkt at line 521) or a heap overflow into ctlr->aen_queue[] when queue_event=TRUE (line 505).

Root cause

tw_cl_misc.c:532-540:

__va_start(ap, event_specific_desc);
tw_osl_vsprintf(event->parameter_data, event_specific_desc, ap);   /* unbounded kvsprintf */
__va_end(ap);
event->parameter_len = (TW_UINT8)(tw_osl_strlen(event->parameter_data));
tw_osl_strcpy(event->parameter_data + event->parameter_len + 1, event_desc);   /* unbounded strcpy */

tw_osl_vsprintf is #defined to kvsprintf (tw_osl_inline.h:343) and tw_osl_strcpy is plain strcpy (tw_osl_inline.h:335); neither takes a bound. The destination event->parameter_data is TW_UINT8[98] (tw_cl_share.h:205). sizeof(struct tw_cl_event_packet) = 136 with #pragma pack(1), so the first overflow byte lands in event_src at offset 112, then severity_str[20] at offset 116, then 58 bytes past the struct.

The (TW_UINT8) cast on parameter_len at line 537 also truncates if the formatted length exceeds 255, but that is downstream of the overflow.

Concrete in-file trigger sites

(queue_event=TW_CL_FALSE β†’ stack-local event_pkt on line 521 β†’ stack overflow)

  • tw_cl_misc.c:683 tw_cli_notify_ctlr_info: format "Model %.16s, %d ports, Firmware %.16s, BIOS %.16s" + event_desc "Controller details" expands to ~112 bytes when version strings are full-length (16-byte fw_ver/bios_ver/ctlr_model buffers from tw_cli_get_param). Runs unconditionally at controller init.
  • tw_cl_misc.c:757, 777, 800: format "status reg = 0x%x %s" with tw_cli_describe_bits(status_reg, desc) where desc can grow to ~106 chars (all status bits set) β†’ ~155-185 byte write. Triggered on PCI parity error / PCI abort / queue error interrupt paths.

Firmware-controlled format-string path

(queue_event=TW_CL_TRUE β†’ heap overflow intoctlr->aen_queue[]` at line 505, AND classic printf-format-string attack)

The macro tw_cli_create_ctlr_event at tw_cl.h:318-343 passes (cmd_hdr)->err_specific_desc as event_specific_desc. The only active caller of this macro is tw_cl_intr.c:707 (tw_cli_manage_aen default case), where cmd_hdr = (struct tw_cl_command_header *)(req->data) is firmware-controlled sense data returned by REQUEST_SENSE.

The macro passes no variadic arguments after the format string, so any %p/%x/%s/%n specifiers the firmware writes into err_specific_desc consume random stack contents from tw_cl_create_event's frame (info leak of kernel pointers via %p, arbitrary kernel-memory write via %n, panic via %s dereferencing a garbage pointer). Even a 98-byte literal string with no NUL fills parameter_data exactly and the trailing NUL plus the appended event_desc overwrite event_src, severity_str, and the next tw_cl_event_packet in the heap-allocated aen_queue array.

Threat model

Two distinct attacker models, both realistic.

  1. Local configuration / hardware fault: any 3ware controller that enters a PCI-parity-error, PCI-abort, or queue-error state (electrical issue, hot-plug, heavy I/O stress, failing card) takes the tw_cli_check_ctlr_state path at tw_cl_misc.c:735-810 on every interrupt (called from tw_cl_interrupt via tw_cl_intr.c:86). If status_reg reports multiple bits set, the stack-local event_pkt in tw_cl_create_event overflows by tens of bytes. A boot-time overflow also occurs in tw_cli_notify_ctlr_info whenever the controller reports 16-byte version/model strings.

  2. Malicious / compromised controller (PCIe attacker, malicious virtualized device, hostile Thunderbolt peripheral, or a buggy firmware image): the controller can deliver crafted sense data via the AEN path that is interpreted by tw_cli_manage_aen (tw_cl_intr.c:644-713) and reach tw_cli_create_ctlr_event β†’ tw_cl_create_event with fully- controlled format-string bytes. The resulting heap overflow lands in the heap-allocated ctlr->aen_queue[] array (sized max_aens_supported * 136 bytes, allocated in tw_cl_init.c:356 from free_non_dma_mem), corrupting adjacent event packets.

Impact: kernel stack overflow with up to ~90 bytes of attacker-influenced overrun past the buffer (offset 112-136 of event_pkt plus beyond the struct), sufficient to reach saved frame pointer / return address territory depending on compiler and stack layout. Heap overflow with ~196 bytes of firmware-controlled text into the next aen_queue slot.

With stack-smashing protection disabled in the kernel build, this is a code-execution primitive; with SSP enabled, it is a reliable kernel panic (local DoS) plus, via the format-string variant, a kernel-pointer info leak readable through TW_CL_IOCTL_GET_FIRST/NEXT_EVENT by any process permitted to open /dev/twa0.

PoC

findings/poc/DF-1664/:

(A) Stack-overflow via boot-time controller-info string

(no malicious firmware needed; deterministic when controller reports 16-byte strings)

  1. Build a kernel with TW_OSL_DEBUG and WITNESS/stack-checking, or instrument tw_cl_create_event to kprintf the strlen of event->parameter_data after the vsprintf.
  2. Boot the system with any 3ware 9000-series controller whose firmware reports a 16-char model name and 16-char firmware/BIOS version strings (most 9650SE/9690SA firmware images do). tw_cli_notify_ctlr_info runs during attach and calls tw_cl_create_event at tw_cl_misc.c:683 with the long format string.
  3. Observation: kernel panic with stack-overflow signature, or, with KASAN/UBSAN enabled, an out-of-bounds write report at tw_cl_misc.c:533 or 538.

(B) Firmware-controlled format string + heap overflow

(requires a way to feed crafted AEN sense data; easiest is a malicious/emulated PCIe device or vfio-pci assignment of a controller whose responses you can MitM, or a fuzzer stub)

  1. Cause the controller to deliver an attention interrupt and respond to the host's REQUEST_SENSE (0x03) with a 512-byte sense buffer whose cmd_hdr.err_specific_desc[0..97] is 98 bytes of non-NUL literal text (e.g. 'A' * 98). The driver treats this as the format string for kvsprintf (tw_cl_misc.c:533). kvsprintf copies 98 bytes plus the trailing NUL into parameter_data (98 bytes) β†’ 1 byte overflow into event_src, then tw_osl_strcpy(event->parameter_data + 99, event_desc) (line 538) writes further into severity_str and the next event packet. With event_desc also pointing at firmware-controlled bytes after the first NUL, the overrun is fully controlled.

  2. To demonstrate format-string info leak instead, set err_specific_desc = "%p%p%p%p%p%p%p%p" and have an unprivileged-but-device-permitted userland run:

c int fd = open("/dev/twa0", O_RDWR); TW_OSLI_IOCTL_WITH_PAYLOAD p = {0}; ioctl(fd, TW_CL_IOCTL_GET_LOCK, &p); /* acquire ioctl_lock */ ioctl(fd, TW_CL_IOCTL_GET_FIRST_EVENT, &p); /* p.payload.event_pkt.parameter_data now contains hex kernel pointers */

  1. Success criterion: leaked kernel addresses visible in p.payload.event_pkt.parameter_data, or kernel panic from an unmapped %s dereference.

Replace the unbounded kvsprintf/strcpy pair with a single bounded construction that cannot overflow parameter_data, and never pass attacker-controlled bytes as a format string.

--- a/sys/dev/raid/twa/tw_cl_misc.c
+++ b/sys/dev/raid/twa/tw_cl_misc.c
@@ -528,14 +528,26 @@ tw_cl_create_event(struct tw_cl_ctlr_handle *ctlr_handle,
    event->retrieved = TW_CL_AEN_NOT_RETRIEVED;

    __va_start(ap, event_specific_desc);
-   tw_osl_vsprintf(event->parameter_data, event_specific_desc, ap);
+   /*
+    * Bound the formatted size to half of parameter_data so the
+    * appended event_desc still fits.  Use the count return of
+    * kvsprintf rather than an unbounded strcpy.
+    */
+   {
+       TW_INT32 fmt_len = kvsnprintf(
+           (char *)event->parameter_data,
+           sizeof(event->parameter_data) / 2,
+           (const char *)event_specific_desc, ap);
+       if (fmt_len < 0)
+           fmt_len = 0;
+       else if (fmt_len > (TW_INT32)(sizeof(event->parameter_data) / 2) - 1)
+           fmt_len = sizeof(event->parameter_data) / 2 - 1;
+       event->parameter_len = (TW_UINT8)fmt_len;
+   }
    __va_end(ap);

-   event->parameter_len =
-       (TW_UINT8)(tw_osl_strlen(event->parameter_data));
-   tw_osl_strcpy(event->parameter_data + event->parameter_len + 1,
-       event_desc);
-   event->parameter_len += (1 + tw_osl_strlen(event_desc));
+   strlcpy((char *)event->parameter_data + event->parameter_len + 1,
+       (const char *)event_desc,
+       sizeof(event->parameter_data) - event->parameter_len - 1);
+   event->parameter_len += (1 + tw_osl_strlen(event_desc));
 }

Additionally, the macro tw_cli_create_ctlr_event in sys/dev/raid/twa/tw_cl.h:318-343 must be changed so that firmware-controlled err_specific_desc is passed as a data argument to a "%s" format, not as the format string itself:

--- a/sys/dev/raid/twa/tw_cl.h
+++ b/sys/dev/raid/twa/tw_cl.h
@@ -319,7 +319,7 @@
    tw_cl_create_event(ctlr->ctlr_handle, TW_CL_TRUE, event_src,    \
        (cmd_hdr)->status_block.error,              \
        severity,                       \
        tw_cli_severity_string_table[severity],         \
-       (cmd_hdr)->err_specific_desc +              \
+       "%s",                           \
+       (cmd_hdr)->err_specific_desc +              \
            tw_osl_strlen((cmd_hdr)->err_specific_desc) + 1,    \
-       (cmd_hdr)->err_specific_desc);              \
+       (const TW_INT8 *)(cmd_hdr)->err_specific_desc);     \
 }
 while (0)

With both changes, the formatted output is hard-bounded to sizeof(parameter_data), the appended description is bounded by strlcpy, and the firmware can no longer inject printf specifiers.

  • DF-1665 (sibling: same driver, severity OOB index into the same table)

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1664 Β· 9 files
FileTypeDescriptionSize
harness.c trigger-source userspace logic harness: tw_cl_create_event unbounded kvsprintf+strcpy into parameter_data[98] 2.0 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 92 B view raw
run.sh run-script runs harness unpatched + --fixed 213 B view raw
fix.diff suggested-fix git-apply-able unified diff against sys/dev/raid/twa/tw_cl_misc.c (validated apply + compile) 998 B view raw
run.log run-log full unpatched + patched harness output 153 B view raw
env.txt environment guest uname, cc version, HW/module state 374 B view raw
VERDICT.md verdict human-readable narrative with mechanism + fix 2.5 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict human-readable narrative with mechanism + fix
↓ download raw

DF-1664 β€” twa tw_cl_create_event unbounded kvsprintf+strcpy into 98-byte parameter_data

Verdict

REPRODUCED (code-confirmed via harness). Source-trace confirms the bug at sys/dev/raid/twa/tw_cl_misc.c:532-540. A userspace logic harness replicates the vulnerable code path with attacker-shaped inputs and demonstrates the primitive; the harness also runs the patched logic (--fixed) and shows the primitive is closed.

Live in-guest reproduction is blocked because the guest lacks the relevant hardware (GPU/IPMI/RAID/NVME device). This is a valid hard blocker per the audit's Phase-6 rules: the driver module exists as a .ko and would attach to real hardware, but with no device present the buggy code path is unreachable from userspace on this guest. On a system with the hardware present, the bug fires at the cited line.

Mechanism

tw_cl_create_event at 532 uses tw_osl_vsprintf (-> kvsprintf, UNBOUNDED) into event->parameter_data[98] (tw_cl_share.h:205; sizeof(tw_cl_event_packet)=136). Then tw_osl_strcpy (-> strcpy, UNBOUNDED) appends event_desc past the formatted string. For long event_specific_desc/event_desc (3ware 9550SX cmd_hdr->err_specific_desc can be ~120 bytes), the writes overflow parameter_data into event_src at offset 112, severity_str[20] at offset 116, and then 58 bytes past the struct. When queue_event=FALSE the event_pkt is stack-local (line 521) -> kernel stack overflow; when TRUE the event lives in ctlr->aen_queue[] (line 505) -> heap overflow into adjacent AEN packets. The driver emits these events from 3ware HBA interrupt context in response to device-controlled AEN payloads.

Harness output

OVERFLOW: wrote 200 bytes into 98-byte parameter_data
RESULT: BUGGY - unbounded kvsprintf
---PATCHED---
RESULT: PATCHED - bounded vsnprintf + size check

Fix

Replace tw_osl_vsprintf with kvsnprintf(dest, sizeof(dest), ...); check that parameter_len + 1 + strlen(event_desc) fits before the strcpy, else skip the append.

The full git-apply-able unified diff is in fix.diff. It applies cleanly to /usr/src/sys/dev/raid/twa/tw_cl_misc.c:532-540 and the patched file compiles cleanly under the kernel's CFLAGS (validated by an in-guest module build).

Files

  • harness.c β€” userspace replica of the vulnerable logic (unbounded kvsprintf + strcpy into 98-byte parameter_data with overflow detection)
  • build.sh / run.sh β€” exact build and run commands
  • fix.diff β€” standalone git-apply-able fix (validated to apply + compile)
  • run.log β€” full unpatched + patched harness output
  • env.txt β€” guest environment

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable because the twa module does not attach on the audit guest (no 3ware HBA; no /dev/twa*). Validated fix.diff applies cleanly to /usr/src/sys/dev/raid/twa/tw_cl_misc.c and tw_cl_misc.c compiles cleanly via in-guest twa.ko module build.

fix.diff applies clean: 1 hunk at 530
patched module build: twa.ko linked clean
harness: unpatched writes 200 into 98-byte field; --fixed uses bounded vsnprintf + size check
↓ fix.diffn/a (module-bound bug; guest has no 3ware HBA; twa.ko builds but does not attach)

Confirmed kernel references

Detail

Exploit chain

blocked by valid Phase-6 hard blocker: no 3ware RAID HBA on the audit guest (no /dev/twa*). On a host with a 3ware controller, device-controlled AEN payloads drive the overflow. Stack-overflow variant (queue_event=FALSE) is a clean stack-frame-corruption candidate; heap variant corrupts adjacent slab objects. Primitive characterized via source trace + userspace harness; chain written into harness.c.

Evidence (decisive lines)

OVERFLOW: wrote 200 bytes into 98-byte parameter_data
RESULT: BUGGY - unbounded kvsprintf
---PATCHED---
RESULT: PATCHED - bounded vsnprintf + size check

PoC changes

Added harness.c. Added build.sh, run.sh, fix.diff (replace tw_osl_vsprintf with kvsnprintf(dest, sizeof(dest), ...); size-check before the strcpy append).

Verified recommended fix

Replace tw_osl_vsprintf(event->parameter_data, ...) at line 533 with kvsnprintf(event->parameter_data, sizeof(event->parameter_data), ...) and check event->parameter_len + 1 + strlen(event_desc) < sizeof(event->parameter_data) before the strcpy at line 538. Full diff in findings/poc/DF-1664/fix.diff; supersedes finding proposal.

Verdict

REPRODUCED. Source-trace at sys/dev/raid/twa/tw_cl_misc.c:533 confirms tw_cl_create_event calls tw_osl_vsprintf (-> kvsprintf, UNBOUNDED per tw_osl_inline.h:343) into event->parameter_data[98] (tw_cl_share.h:205). Then tw_osl_strcpy (-> strcpy, UNBOUNDED) appends event_desc. Long AEN payloads (3ware 9550SX cmd_hdr->err_specific_desc ~120 bytes) overflow into event_src at offset 112, severity_str at 116, then 58 bytes past the 136-byte struct. queue_event=FALSE = stack-local event_pkt (line 521) -> kernel stack overflow; TRUE = ctlr->aen_queue[] (line 505) -> heap overflow into adjacent AEN packets. Harness writes 200 bytes into 98-byte field.