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:683tw_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-bytefw_ver/bios_ver/ctlr_modelbuffers fromtw_cli_get_param). Runs unconditionally at controller init.tw_cl_misc.c:757, 777, 800: format"status reg = 0x%x %s"withtw_cli_describe_bits(status_reg, desc)wheredesccan 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.
-
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_statepath attw_cl_misc.c:735-810on every interrupt (called fromtw_cl_interruptviatw_cl_intr.c:86). Ifstatus_regreports multiple bits set, the stack-localevent_pktintw_cl_create_eventoverflows by tens of bytes. A boot-time overflow also occurs intw_cli_notify_ctlr_infowhenever the controller reports 16-byte version/model strings. -
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 reachtw_cli_create_ctlr_event β tw_cl_create_eventwith fully- controlled format-string bytes. The resulting heap overflow lands in the heap-allocatedctlr->aen_queue[]array (sizedmax_aens_supported * 136bytes, allocated intw_cl_init.c:356fromfree_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)
- Build a kernel with
TW_OSL_DEBUGandWITNESS/stack-checking, or instrumenttw_cl_create_eventtokprintfthestrlenofevent->parameter_dataafter thevsprintf. - 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_inforuns during attach and callstw_cl_create_eventattw_cl_misc.c:683with the long format string. - Observation: kernel panic with stack-overflow signature, or, with
KASAN/UBSANenabled, an out-of-bounds write report attw_cl_misc.c:533or538.
(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)
-
Cause the controller to deliver an attention interrupt and respond to the host's
REQUEST_SENSE(0x03) with a 512-byte sense buffer whosecmd_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 forkvsprintf(tw_cl_misc.c:533).kvsprintfcopies 98 bytes plus the trailing NUL intoparameter_data(98 bytes) β 1 byte overflow intoevent_src, thentw_osl_strcpy(event->parameter_data + 99, event_desc)(line 538) writes further intoseverity_strand the next event packet. Withevent_descalso pointing at firmware-controlled bytes after the first NUL, the overrun is fully controlled. -
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 */
- Success criterion: leaked kernel addresses visible in
p.payload.event_pkt.parameter_data, or kernel panic from an unmapped%sdereference.
Recommended fix
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.
Related findings
- DF-1665 (sibling: same driver,
severityOOB index into the same table)
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1664 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 commandsfix.diffβ standalone git-apply-able fix (validated to apply + compile)run.logβ full unpatched + patched harness outputenv.txtβ guest environment
Fix verification
not_testablenot_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
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- t
- w
- a
- /
- t
- w
- _
- c
- l
- _
- m
- i
- s
- c
- .
- c
- :
- 5
- 3
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- t
- w
- a
- /
- t
- w
- _
- c
- l
- _
- m
- i
- s
- c
- .
- c
- :
- 5
- 3
- 8
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- t
- w
- a
- /
- t
- w
- _
- c
- l
- _
- s
- h
- a
- r
- e
- .
- h
- :
- 2
- 0
- 5
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.
No comments yet.