twa(4): tw_cli_drain_aen_queue() re-queues a firmware-owned request into FREE_Q on ETIMEDOUT β request left dual-linked in BUSY_Q/PENDING_Q + FREE_Q (request handed out twice, internal DMA buffer re-assigned, crossed completions; third site of the DF-4335/DF-4274/DF-1814 class)
Summary
On the post-reset AEN drain, a REQUEST_SENSE the wedged firmware never completes times out and breaks to out:, which unconditionally inserts the request into FREE_Q although it is still linked in BUSY_Q or PENDING_Q - the insert macros overwrite next/prev without unlinking, so the request becomes a member of two cross-linked rings. get_request then hands the still-in-flight request to a second owner, zeroes its flags/callback while firmware DMA is live, and the eventual completion completes the reused request into the wrong owner and double-inserts into FREE_Q. Requires a hung 9000-series controller (the reset path that calls this). Unlink-before-requeue fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4509 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.4 KB | β raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| twa_probe.c | β | 2.4 KB | view raw | |
| build.sh | β | 108 B | view raw | |
| run.sh | β | 95 B | view raw | |
| build.log | β | 28 B | view raw | |
| run.log | β | 932 B | view raw | |
| env.txt | β | 419 B | view raw | |
| fix.diff | β | 740 B | view raw | |
| verdict.json | β | 3.7 KB | view raw |
DF-4509 β twa(4) tw_cli_drain_aen_queue() FREE_Q double-insert on ETIMEDOUT (request left dual-linked in BUSY_Q/PENDING_Q + FREE_Q)
- File:
sys/dev/raid/twa/tw_cl_misc.c:309-386(out: label:379-384, ETIMEDOUT break:353-356) - Class: queue-discipline / latent use-after-free (memcorrupt bucket)
- Siblings: DF-4335 (get/set_param requeue), DF-4274 (OSL passthru), DF-1814 (init_connection)
What it is
When the 3ware firmware stops completing commands, the controller-reset path
calls tw_cli_drain_aen_queue(), which submits a REQUEST_SENSE by internal
request and polls for TW_CLI_REQUEST_TIMEOUT_PERIOD. The request is at that
moment linked into TW_CLI_BUSY_Q (tw_cl_io.c:219) or
TW_CLI_PENDING_Q (tw_cl_io.c:204). If the poll times out, the function
breaks to its out: label and unconditionally inserts the same request into
TW_CLI_FREE_Q β without unlinking it from the queue it still occupies. The
list macros (TW_CL_Q_INSERT_TAIL, tw_cl_share.h:284-289) overwrite the
item's next/prev, cross-linking the two rings: the request can subsequently be
handed out by tw_cli_get_request() while the firmware still owns it, its
internal_req_data DMA buffer is re-assigned to a second command, and the
later firmware completion / reset drain completes the reused request into the
wrong owner's callback. Same corruption class as DF-4335/DF-4274, third
distinct site.
Reproduce (on twa-equipped hardware)
./build.sh && ./run.shβ run.sh is a presence prober; it lists/dev/twa*.- Real trigger: hang the controller firmware (fatal drive-slot fault, or
devctl-detach the device MMIO region while I/O is in flight) so the watchdog/passthru-timeout reset path fires; the reset's post-reset AEN drain then hits the 60s REQUEST_SENSE timeout and the request is double-queued. Expected observation: subsequentkldunload/reset hangs, duplicated completions in the trace buffer, or a panic in the request queue walk.
On the audit guest
twa_probe reports no /dev/twa* nodes ... controller ABSENT. The QEMU
guest has no 3ware 9000-series PCI device; the driver never attaches. The
defect is proven by code inspection in VERDICT.md (deterministic queue-state
analysis); verdict: inconclusive (hardware absent), not a false positive.
Fix
fix.diff β unlink the request from BUSY_Q/PENDING_Q at the out: label
before inserting it into FREE_Q.
DF-4509 β twa(4) tw_cli_drain_aen_queue() re-queues a still-busy request into FREE_Q on ETIMEDOUT β dual queue membership β request reuse / list corruption (latent UAF)
Status: inconclusive on the audit guest (no twa hardware). The defect is proven deterministically by code inspection below; it cannot be exercised on a machine without a 3ware 9000-series controller, and even with one it requires the controller firmware to stop completing commands (the precise failure mode that triggers the reset path in the first place).
Root cause (line-accurate)
tw_cli_drain_aen_queue() β sys/dev/raid/twa/tw_cl_misc.c:309-386:
319: if ((req = tw_cli_get_request(ctlr)) == TW_CL_NULL) { ... } // req UNLINKED from FREE_Q
325: req->flags |= TW_CLI_REQ_FLAGS_INTERNAL;
326: req->tw_cli_callback = TW_CL_NULL;
327: if ((error = tw_cli_send_scsi_cmd(req, 0x03 /* REQUEST_SENSE */))) ... break;
// tw_cl_io.c:1304-1308: internal_req_busy=TRUE; req->data=internal buffer
// tw_cl_io.c:1340 -> tw_cli_submit_cmd(req):
// queue full -> tw_cl_io.c:199-204: state=PENDING; insert_tail(req, PENDING_Q)
// submitted ok -> tw_cl_io.c:218-219: state=BUSY; insert_tail(req, BUSY_Q)
337: do { ... poll tw_cli_process_resp_intr(req->ctlr) ... } while (time <= end_time);
353: if (req->state != TW_CLI_REQ_STATE_COMPLETE) {
354: error = TW_OSL_ETIMEDOUT; // <-- firmware never completed the sense
355: break; // req STILL LINKED in BUSY_Q or PENDING_Q
356: }
...
379: out:
380: if (req) {
381: if (req->data)
382: ctlr->internal_req_busy = TW_CL_FALSE; // internal DMA buffer also released
383: tw_cli_req_q_insert_tail(req, TW_CLI_FREE_Q); // <-- SECOND membership, no unlink
384: }
The queue-insert macros (TW_CL_Q_INSERT_TAIL, sys/dev/raid/twa/tw_cl_share.h:284-289)
blindly overwrite item->next/item->prev and link the item into the new
ring. They do not unlink the item from any list it is still on. After the
ETIMEDOUT break, req is simultaneously:
- linked in
TW_CLI_BUSY_Q(viatw_cl_io.c:219) orTW_CLI_PENDING_Q(viatw_cl_io.c:204), and - linked in
TW_CLI_FREE_Q(viatw_cl_misc.c:383),
with BUSY_Q/PENDING_Q's neighbours still pointing at req while req's own
pointers now describe FREE_Q's ring β the two rings are cross-linked.
Consequence chain
tw_cli_get_request()(tw_cl_misc.c:578) popsreqfrom FREE_Q and hands it to a new owner (e.g. the next passthru ioctl, AEN fetch, or param request), zeroingreq->flags,req->orig_req,req->tw_cli_callback(tw_cl_misc.c:583-591) whilereqis still recorded as in-flight with the firmware.internal_req_busywas cleared attw_cl_misc.c:382although the original REQUEST_SENSE still ownsctlr->internal_req_data(assigned attw_cl_io.c:1305); the next internal command reuses the same DMA buffer for a different command while the firmware may still write the old one.- When the hung firmware eventually completes (or a subsequent reset drains
queues):
tw_cli_process_resp_intr(tw_cl_intr.c:238-260) looks the request up by firmware id, findsstate == BUSY(it was re-submitted by the new owner), removes it from BUSY_Q, and completes it into the new owner's callback context; ortw_cli_drain_busy_queue(tw_cl_misc.c:140-168) completes the same request again for the old context. Onestruct tw_cli_req_contextwith two owners β crossed completion callbacks, double insertion into FREE_Q from both contexts (the exact corruption class of DF-4335/DF-4274), and eventually the request-ring macros writing through stale neighbour pointers once the duplicate is unlinked twice (TW_CL_Q_REMOVE_ITEM,tw_cl_share.h:293-296) β latent memory corruption of the driver's static request array and a request reused while its 2048-bytecmd_pktis under live firmware DMA (tw_cl_init.c:341-346maps each request to its own command packet).
Why inconclusive on the guest
$ ls /dev/twa* -> No such file or directory $ pciconf -l -> no 0x13C1/0x1002-class 3ware 9xxx device
The audit QEMU guest has no twa controller; the driver never attaches, so the reset/AEN machinery never executes. There is no unprivileged (or even root-usable) software trigger short of the hardware hanging.
Family / siblings (not duplicates)
- DF-4335 β same corruption class, different sites:
tw_cli_get_param/tw_cli_set_param(tw_cl_io.c) re-queue after ETIMEDOUT during normal operation (every AEN sync-time, every management ioctl). - DF-4274 β OSL passthru deferred-busdma error path returns a request to
FREE_Q twice (
tw_osl_freebsd.c). - DF-1814 β
tw_cli_init_connectiondouble-insert on submit_and_poll failure. - This finding (DF-4509) β the reset-path AEN drain in the misc layer, reachable exactly when the controller is being recovered from a hang.
Recommended fix
Unlink the request from whichever queue it still occupies before returning it
to FREE_Q at the out: label β see fix.diff.
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_testablefix.diff (unlink from BUSY_Q/PENDING_Q before the FREE_Q insert at the out: label) is git-apply clean against the read-only sys/ tree and uses only existing macros (tw_cli_req_q_remove_item, tw_cl.h:305); baseline/patched behaviour could not be compared because no twa controller exists on the guest and the vulnerable path needs a hung firmware to fire.
git apply --check findings/poc/DF-4509/fix.diff -> OK; macro/state constants verified against sys/dev/raid/twa/tw_cl.h:59-62,270,305
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (full line-by-line queue-state proof); twa_probe.c + run.log (device-absent diagnosis); fix.diff (git-apply clean against sys/)
PoC changes
authored fresh: device-presence prober documenting the hardware precondition (no seed existed)
Verified recommended fix
At tw_cl_misc.c out: label, tw_cli_req_q_remove_item(req, BUSY_Q or PENDING_Q) when req->state is still BUSY/PENDING before inserting into FREE_Q
Verdict
Code-certain queue-discipline defect, hardware-gated: on the ETIMEDOUT path of tw_cli_drain_aen_queue (tw_cl_misc.c:353-356 -> out: label at :379-384) the internal REQUEST_SENSE request is inserted into TW_CLI_FREE_Q while it is still linked in TW_CLI_BUSY_Q (tw_cl_io.c:219) or TW_CLI_PENDING_Q (tw_cl_io.c:204) because the insert macros (tw_cl_share.h:284-296) never unlink; the request is then handed out twice (tw_cli_get_request) while the firmware still owns it and internal_req_data is re-assigned, producing crossed completions, double FREE_Q insertion from two owners and request reuse under live firmware DMA (same class as DF-4335/DF-4274/DF-1814, distinct site in the reset path). Deterministic once the firmware fails to complete the sense within the 60s poll window. Could not be exercised: the audit QEMU guest has no 3ware 9000-series controller (no /dev/twa*, prober exit 2 in run.log), and even on hardware the trigger is a hung controller during reset, not a userspace action. Not a false positive: the state machine analysis in VERDICT.md is deterministic.
No comments yet.