# 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-813**
* `tw_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:

1. The doubly-inserted node's `next` points 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 the `gen_lock` spinlock** →
   system-wide wedge of every CPU touching any CL request queue.
2. Once the victim reaches the head, every `remove_head` returns the *same*
   `struct tw_cli_req_context` → two owners build commands in one context
   (`tw_cli_get_request()` resets flags but cannot stop the second handout).
3. For the BUSY-timeout variant the firmware still owns the DMA of
   `req->cmd_pkt` while a new owner rewrites it; the late firmware response
   then completes the wrong request, and `tw_cli_complete_io()`
   (tw_cl_intr.c:407-416) copies 1024 bytes of firmware response into
   `req_pkt->gen_req_pkt.pt_req.cmd_pkt` of 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/twaN` is 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`).
