# 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

1. `tw_cli_get_param()` builds an internal request, poll-submits it:
   - `sys/dev/raid/twa/tw_cl_io.c:781-784`
     ```c
     error = tw_cli_submit_and_poll_request(req, TW_CLI_REQUEST_TIMEOUT_PERIOD);
     if (error)
         goto out;
     ```
2. `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**,
     returns `TW_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"*.
3. The caller ignores the contract and cleans up anyway:
   - `tw_cl_io.c:804-814` (`get_param` `out:`) —
     `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_param` `out:`) — 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) sets `item->next = head` and
  `item->prev = head->prev` where `head->prev` *is 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.log` Case 1) and once the node reaches the head, every
  `tw_cli_req_q_remove_head()` returns it again (Case 1, "handed out 2
  time(s)").  Queue ops hold `ctlr->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 resets
  `state/flags` but nothing prevents the second handout), its `cmd_pkt` DMA
  buffer is rebuilt while the firmware still owns the original command, and
  the late response for the old `request_id` completes whichever request
  currently sits in that context — including the 1024-byte copyback at
  `tw_cl_intr.c:407-410` into 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.
