twa CL: tw_cli_get_param/tw_cli_set_param re-queue a request after ETIMEDOUT β FREE_Q double-insert (unwalkable queue, spin under gen_lock) or BUSY_Q+FREE_Q dual membership (request reuse under live firmware DMA)
Summary
tw_cli_submit_and_poll_request documents an ownership contract on poll timeout: a PENDING request is cleaned up by the helper itself, a BUSY request stays firmware-owned, and 'the caller is expected to NOT cleanup when ETIMEDOUT is returned'. Both poll-mode callers violate it with unconditional FREE_Q inserts on their out: paths. PENDING timeout => the same node inserted twice - the second INSERT_TAIL self-loops the node, the free queue becomes unwalkable (in-kernel: remove_head spins forever holding the gen_lock spinlock) and every remove_head hands the same context to a new owner. BUSY timeout => one request simultaneously in BUSY_Q and FREE_Q: re-issued while the firmware still owns its DMA, the late response completing whichever request now occupies that context (1024-byte firmware copyback into the wrong OSL buffer). Single-threaded, deterministic. Logic-model reproducer built/run on the guest with the driver's verbatim queue macros: self-loop + dual-membership shown; fix-modeled clean. Requeue-callback fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4335 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | file | 5.1 KB | β raw | |
| VERDICT.md | file | 5.5 KB | β raw | |
| build.log | file | 255 B | view raw | |
| build.sh | file | 162 B | view raw | |
| env.txt | file | 376 B | view raw | |
| fix.diff | file | 2.8 KB | view raw | |
| manifest.json | file | 1.8 KB | view raw | |
| poc_dblinsert.c | file | 9.4 KB | view raw | |
| run.log | file | 579 B | view raw | |
| run.sh | file | 91 B | view raw | |
| verdict.json | file | 4.0 KB | view raw |
DF-4335 β twa CL get/set_param poll-timeout cleanup-contract violation
File: sys/dev/raid/twa/tw_cl_io.c
Class: memcorrupt (request-queue corruption β cross-thread request reuse)
Severity: Medium β hardware-gated (3ware 9000-series controller), corruption-grade mechanism
Status: kernel path NOT runnable on the audit guest (no twa hardware); mechanism
reproduced deterministically at logic level with the driver's own queue macros.
What
tw_cli_get_param() and tw_cli_set_param() (poll mode, callback == TW_CL_NULL)
submit an internal firmware request via tw_cli_submit_and_poll_request(). On
poll timeout that helper enforces an explicit ownership contract
(tw_cl_io.c:999-1006):
"If a busy request timed out, the reset will take care of freeing resources. If a pending request timed out, we will free resources for that request, right here β¦ So, the caller is expected to NOT cleanup when TW_OSL_ETIMEDOUT is returned."
Both callers violate it: their out: label unconditionally re-queues the
request (tw_cli_req_q_insert_tail(req, TW_CLI_FREE_Q)) whatever the error:
tw_cli_get_paramβ submit at tw_cl_io.c:781-784, re-queue at 812-813tw_cli_set_paramβ submit at tw_cl_io.c:899-902, re-queue at 929-930
Combined with the helper's own cleanup:
- PENDING timeout β helper already inserted the request into FREE_Q at tw_cl_io.c:1029 β the caller inserts it a second time.
- BUSY timeout β the request is still linked in BUSY_Q and owned by the firmware; the caller links the same node into FREE_Q β dual queue membership while in flight.
Every insert/remove is individually gen_lock-protected (tw_cl.h:270-313),
so the second insert is not rejected β it is performed. Consequences, in
escalating order:
- The doubly-inserted node's
nextpoints at itself β the FREE queue is no longer walkable;tw_cli_req_q_remove_head()called from the MPSAFE interrupt path spins forever while holding thegen_lockspinlock β system-wide wedge of every CPU touching any CL request queue. - Once the victim reaches the head, every
remove_headreturns the samestruct tw_cli_req_contextβ two owners build commands in one context (tw_cli_get_request()resets flags but cannot stop the second handout). - For the BUSY-timeout variant the firmware still owns the DMA of
req->cmd_pktwhile a new owner rewrites it; the late firmware response then completes the wrong request, andtw_cli_complete_io()(tw_cl_intr.c:407-416) copies 1024 bytes of firmware response intoreq_pkt->gen_req_pkt.pt_req.cmd_pktof whichever OSL request last owned the context β cross-request kernel-memory corruption.
Siblings already filed (same class, different sites): DF-1814 (tw_cli_init_connection, tw_cl_init.c), DF-4274 (OSL deferred-busdma path).
Trigger preconditions
- 3ware 9000/9550SX/9650SE controller present (twa(4) attached).
- An internal poll-mode request must time out (60 s):
- at attach (
tw_cli_notify_ctlr_infoβ get_param, tw_cl_misc.c:671-680) with a wedged/slow firmware, or - during a controller reset (
tw_cl_reset_ctlrβtw_cli_soft_resetβtw_cli_drain_aen_queue/SYNC_TIME β set_param, interrupts down). - No unprivileged reachability:
/dev/twaNis 0600 root:operator (tw_osl_freebsd.c:405-406); the attach/reset paths need no ioctl at all β a wedged firmware alone is enough.
How to reproduce (logic model β no hardware needed)
cd findings/poc/DF-4335 ./build.sh # cc -O2 -Wall -o poc_dblinsert poc_dblinsert.c ./run.sh # ./poc_dblinsert
Expected output (observed on the audit guest, DragonFly 6.5-DEVELOPMENT x86_64, cc 8.3):
Case 1 (PENDING timeout): FREE_Q walk=INFINITE CYCLE (queue unwalkable; in-kernel this spins while holding the gen_lock spinlock) <-- BUG consumers received 5 request(s); victim handed out 2 time(s) -> SAME REQUEST CONTEXT ISSUED TO TWO OWNERS <-- BUG Case 2 (BUSY timeout): victim on BUSY_Q=1 on FREE_Q=1 -> REQUEST SIMULTANEOUSLY IN BUSY_Q AND FREE_Q <-- BUG Case 3 (PENDING, fixed): FREE_Q walk=4 victim_on_free=1 (expect 4 / 1, no cycle) Case 3 (BUSY, fixed): victim on BUSY_Q=1 on FREE_Q=0 (expect 1 / 0; reset-drain hook requeues once later)
Cases 1-2 model the shipping code (corruption); Case 3 models the call sequence after fix.diff (clean single membership in both timeout cases).
The reproducer uses the driver's exact TW_CL_Q_* list macros copied from
tw_cl_share.h and the exact call sequence of tw_cl_get_param's error path
vs tw_cli_submit_and_poll_request's timeout tail. It is a faithful model of
the queue-state machine, not of the firmware interaction; the kernel path
itself cannot be exercised on a guest without the PCI hardware.
Fix
See fix.diff β callers must not re-queue on TW_OSL_ETIMEDOUT (PENDING
case is already cleaned by the helper), and the BUSY case needs a completion
hook so the imminent reset drain re-queues the request exactly once
(the documented contract is otherwise unimplemented for poll-mode requests,
which have no tw_cli_callback).
DF-4335 β VERDICT
Status: untested (kernel path not runnable β no twa(4) hardware on the
audit guest; /dev/twa* absent, no PCI vendor 0x13c1 device present).
Mechanism: reproduced deterministically at logic level
(poc_dblinsert, run on the guest, run.log).
Impact class of the defect: request-queue corruption β cross-thread
request-context reuse β kernel memory corruption / hard lockup; realistic
manifestation on real hardware is panic/wedge (DoS-grade) with a corruption
path that cannot be ruled out.
Confidence: certain for the code defect (single-threaded, deterministic,
no race required β only a 60 s poll timeout of an internal request).
Root cause, line by line
tw_cli_get_param()builds an internal request, poll-submits it: -sys/dev/raid/twa/tw_cl_io.c:781-784c error = tw_cli_submit_and_poll_request(req, TW_CLI_REQUEST_TIMEOUT_PERIOD); if (error) goto out;tw_cli_submit_and_poll_request()on timeout, PENDING case, cleans up the request itself: -sys/dev/raid/twa/tw_cl_io.c:1014-1030β removes from PENDING queue,tw_cli_req_q_insert_tail(req, TW_CLI_FREE_Q)at 1029, returnsTW_OSL_ETIMEDOUT. - The ownership contract is written out at 999-1006: "the caller is expected to NOT cleanup when TW_OSL_ETIMEDOUT is returned"; for the BUSY case "the reset will take care of freeing resources".- The caller ignores the contract and cleans up anyway:
-
tw_cl_io.c:804-814(get_paramout:) βtw_cli_req_q_insert_tail(req, TW_CLI_FREE_Q)at 812-813, unconditional on the error value. -tw_cl_io.c:921-931(set_paramout:) β same, at 929-930.
Result by case:
- PENDING timeout: node inserted into FREE_Q twice. The second
TW_CL_Q_INSERT_TAIL(tw_cl_share.h:288-296) setsitem->next = headanditem->prev = head->prevwherehead->previs the same node (it is already the tail) βv->next = v: the circular list now has a self-loop. Verified with the driver's own macros: walking the queue never terminates (run.logCase 1) and once the node reaches the head, everytw_cli_req_q_remove_head()returns it again (Case 1, "handed out 2 time(s)"). Queue ops holdctlr->gen_lockβ a spinlock (tw_cl.h:274, spin_init at tw_osl_freebsd.c:307) β so in-kernel the walk spins with preemption disabled. - BUSY timeout: the request is still linked in BUSY_Q and owned by the
firmware; the caller's insert links the same node into FREE_Q β
simultaneous membership (run.log Case 2). The request is then handed out
again by
tw_cli_get_request()(tw_cl_misc.c:569-606; it resetsstate/flagsbut nothing prevents the second handout), itscmd_pktDMA buffer is rebuilt while the firmware still owns the original command, and the late response for the oldrequest_idcompletes whichever request currently sits in that context β including the 1024-byte copyback attw_cl_intr.c:407-410into the wrong OSL request's buffer.
Why the reset does not save the PENDING case either
For a poll-mode (callback == NULL) request, req->tw_cli_callback is NULL,
so tw_cli_drain_busy_queue (tw_cl_misc.c:140-170) sets error_code and
calls no callback β the documented "reset will take care of freeing
resources" is unimplemented for poll-mode requests. The request would leak
in no queue at all were the caller's unconditional insert removed; hence the
fix adds a requeue callback for the BUSY-timeout case instead of merely
deleting the caller's insert (see fix.diff).
Why untested (not not_reproduced / not_testable)
The defect is in the twa(4) common layer; reaching it requires a 3ware
9000-series PCI controller (attach path) or a controller reset (wedged
firmware). The audit QEMU guest has no such device:
ls /dev/twa* β No such file or directory; pciconf -l | grep -c 13c1 β 0
(build.log). There is no software-only entry point: /dev/twaN exists only
when the driver attached to real hardware. The logic-level reproduction
(poc_dblinsert) was built and run on the guest itself to demonstrate the
queue-state machine corruption with the driver's own list macros.
Exploit chain (hardware present, outline)
operator/root or wedged-firmware-at-boot β internal get/set_param request times out after 60 s with PENDING (or BUSY) state β FREE_Q self-loop / dual-queue membership β (a) wedge: any CPU taking gen_lock spins forever (hard DoS), or (b) same request context handed to two owners β cmd_pkt rewritten under live firmware DMA + late response completing the wrong request β 1024-byte firmware-controlled copyback into a foreign OSL request buffer (kernel heap corruption). Deterministic escalation beyond panic is hardware- and firmware-dependent; not demonstrable on this guest.
Fix validation
Not possible on this guest (no twa hardware): fix_status: not_testable,
fix_baseline_reproduced: 0, fix_patched_reproduced: 0. fix.diff was
authored against the read-only sys/ tree after verification; it was never
applied there. The logic model can be re-pointed at the patched call
sequence to show both cases return to single-queue membership.
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_testableKernel fix validation impossible on this guest (no twa hardware). fix.diff is git-apply-valid against sys/; the model reproducer's Case 3 demonstrates the patched call sequence restores single-queue membership in both timeout variants.
findings/poc/DF-4335/fix.diff (git apply --check PASS); run.log Case 3 lines
Confirmed kernel references
Detail
Exploit chain
3ware controller present + internal get/set_param poll request times out (wedged/slow firmware at attach or during controller reset; no ioctl needed) -> FREE_Q double-insert or BUSY+FREE dual membership -> (a) any CPU touching a CL request queue spins forever on gen_lock (hard wedge), or (b) same request context handed to two owners -> cmd_pkt rewritten while firmware owns the DMA -> late firmware response completes the wrong request -> 1024-byte firmware-controlled copyback into wrong kernel buffer. Not exercisable on this guest.
Evidence (decisive lines)
run.log (guest-executed model reproducer: Case 1 INFINITE CYCLE + 'handed out 2 time(s)', Case 2 dual-queue membership, Case 3 fixed-sequence clean); VERDICT.md (line-by-line trace); fix.diff (git-apply-valid against sys/)
PoC changes
No hardware PoC possible; authored a deterministic logic-model reproducer using the driver's verbatim TW_CL_Q_* macros (tw_cl_share.h) modeling submit_and_poll's timeout tail vs get/set_param's out: path; bounded the queue walkers with cycle guards so the (deliberately corrupted) list walk terminates and reports the cycle instead of hanging.
Verified recommended fix
get_param/set_param out: paths must not re-queue (nor clear internal_req_busy) on TW_OSL_ETIMEDOUT; give BUSY-timed-out poll-mode requests a tw_cli_poll_requeue_callback so the next reset drain re-queues them exactly once (fix.diff).
Verdict
Kernel path not runnable on this guest (no 3ware 9000-series hardware: no /dev/twa, 0 PCI 13c1 devices), but the defect is a deterministic single-threaded logic error, and its queue-corruption mechanism was reproduced on the guest with the driver's own TW_CL_Q_ list macros: after a 60s poll timeout of a PENDING internal request, tw_cli_get_param/tw_cli_set_param insert the request into FREE_Q a second time (helper already inserted it at tw_cl_io.c:1029), producing an unwalkable self-looped free queue (in-kernel: unbounded spin while holding the gen_lock spinlock) and repeated handout of the same request context to two owners; the BUSY-timeout variant leaves one request simultaneously in BUSY_Q and FREE_Q, so its cmd_pkt DMA buffer is rebuilt under live firmware ownership and the late response completes the wrong request including a 1024-byte copyback into a foreign OSL buffer (tw_cl_intr.c:407-416). Sibling of DF-1814/DF-4274 at a distinct site in this file.
No comments yet.