β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4400

tws_intr_resp invokes req->cb with no busy-state/completer validation β€” firmware replay of an in-range req_id double-completes surrendered ccbs or calls a NULL cb (distinct from DF-1590's index OOB)

Summary

The response walk calls reqs[req_id].cb for every firmware-supplied in-range id without checking the request is in flight. cb is only reset on the NEXT allocation and never on release, so a duplicate/stale id re-runs tws_scsi_complete on a completed slot (second xpt_done, callout_stop on dead ccb) or early-completes a reallocated slot's live ccb. get_param additionally submits a NULL-cb req (latent). Loop terminates only when the firmware stops producing responses. Validate-state+cb dispatch fix (also closes DF-1590's window at this site).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4400 Β· 6 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 2.8 KB ↓ raw
trigger.c β€” 712 B view raw
fix.diff β€” 1.0 KB view raw
verdict.json β€” 2.6 KB view raw
env.txt β€” 541 B view raw

DF-4400 β€” tws_intr_resp invokes req->cb with no state validation (double completion / NULL cb)

What

sys/dev/raid/tws/tws_cam.c:1156-1173:

while ( tws_get_response(sc, &req_id, &mfa) ) {
    ...
    if ( req_id == TWS_INVALID_REQID ) { ... tws_err_complete(sc, mfa); continue; }
    sc->reqs[req_id].cb(&sc->reqs[req_id]);   /* <-- no bounds, no state check */
}

Two distinct defects beyond DF-1590 (which covers the out-of-bounds index):

  1. No busy-state validation. req->cb is only initialized when a request is allocated (tws_get_request, tws.c:803-814, which also sets cb = NULL) or explicitly by the submitter; it is never cleared on release. A firmware-fed in-range req_id that is stale (duplicate response for an already-completed request, or a response for a FREE slot) invokes the old callback: - stale tws_scsi_complete (tws_cam.c:738) on a completed request β†’ xpt_done(ccb) a second time on a ccb CAM may already have recycled, and callout_stop(req->ccb_ptr->ccb_h.timeout_ch) on dead ccb memory; - if the slot was reallocated, req->ccb_ptr is the new, in-flight ccb β†’ the new transaction is completed early while the firmware still owns it (biodone of an in-flight bio β€” the tws/twa sibling bug DF-4178's signature).
  2. NULL callback call. tws_get_param() deliberately submits with req->cb = NULL (tws_cam.c:924) because it polls for the response (:941-942). If an interrupt ever delivers that response first, sc->reqs[3].cb(...) is a NULL function-pointer call β†’ panic. (Today this is latent: tws_get_param runs only at attach with interrupts masked, tws.c:547 / tws_hdm.c:112-114 β€” but nothing enforces it.)

Impact

Device-fed trigger (misbehaving/duplicating firmware β€” same threat model as DF-1590): kernel NULL-call panic or CAM use-after-free / double-done list corruption. The response loop also terminates only when the firmware stops producing responses (tws_get_response false), so a hostile controller can keep the interrupt handler spinning through arbitrary stale callbacks.

Trigger

No local trigger β€” requires the controller to emit a duplicate/stale response. Documented in VERDICT.md; trigger.c is a compile-only stub that documents the expected response word (16-bit request_id bitfield, tws_hdm.h:348-353).

Status

QEMU guest has no 3ware HBA β†’ inconclusive; logic proven from source.

VERDICT.md
↓ download raw

VERDICT β€” DF-4400

Status: inconclusive (hardware-gated). The defect is certain from source; triggering requires the 3ware controller to emit a duplicate or stale response (same device-fed trust that DF-1590 documents for the OOB-index aspect).

Proof chain

  1. tws_get_response() (tws_hdm.c:354-381) extracts request_id from a 16-bit firmware bitfield (struct tws_outbound_response, tws_hdm.h:348-353).
  2. tws_intr_resp() (tws_cam.c:1161-1171) treats every non-TWS_INVALID_REQID value as an in-flight request and calls sc->reqs[req_id].cb(...) β€” no bounds check (that is DF-1590) and no check that the request is BUSY / has a completer.
  3. Request lifecycle (tws.c:744-828): cb is assigned per submission (tws_cam.c:738, 829, 875; tws_user.c:124) and reset to NULL only on the next allocation (tws.c:812); release paths (tws_q_insert_tail to FREE_Q at tws_scsi_complete, tws_cam.c:374-376) never clear it, and reserved slots 0-3 are gated by state alone (never queued).
  4. Consequence for a stale/duplicate in-range id: - completed slot: tws_scsi_complete runs again β†’ tws_q_remove_request harmlessly returns NULL (tws_services.c:314-362) but callout_stop(req->ccb_ptr->ccb_h.timeout_ch) (tws_cam.c:365) touches a ccb already surrendered via xpt_done (:371), and xpt_done runs a second time (:371) β†’ CAM done-queue double-insert / UAF (ccb recycled by periph between the two completions). - reallocated slot: early xpt_done of the new, firmware-owned ccb β†’ biodone of an in-flight bio (identical class to twe DF-4178).
  5. NULL-cb instance: tws_get_param submits reqs[3] with cb = NULL (tws_cam.c:924, submit at :941) and polls (:942); any interrupt that delivers that response first (tws_intr_resp) calls NULL β†’ panic. Currently masked because tws_get_param runs only during attach (tws_hdm.c:218-225 β†’ tws_init_ctlr:112) with interrupts off (tws.c:547, turned on only at tws_hdm.c:114).

Impact ceiling

Panic (NULL cb, double-done) or CAM-layer memory corruption from device-controlled completion replay. Not locally triggerable by an unprivileged user; requires errant/hostile firmware β€” rated Medium to stay consistent with DF-1590 (same trust boundary, comparable primitive).

Fix

Validate the request before invoking the callback (and, belt-and-braces, the bounds β€” see fix.diff, which also closes DF-1590's window at this call site).

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_testable
baseline no→ patch + rebuild →patched clean

fix.diff adds index+state+completer validation at the response-walk call site (also closes DF-1590's window at this site); not validatable without the controller.

fix.diff
↓ fix.diffper-fix-DF-4400

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (lifecycle proof); fix.diff (state+bounds guard at the call site, also closing DF-1590's window here); trigger.c (response-word documentation)

PoC changes

no seed; documentation stub only (no local trigger exists without the controller)

Verified recommended fix

In tws_intr_resp, validate req_id < tws_queue_depth, cb != NULL and state != TWS_REQ_STATE_FREE before invoking the callback.

Verdict

Certain from source, hardware-gated to trigger. tws_intr_resp (tws_cam.c:1170) calls sc->reqs[req_id].cb() on every firmware-supplied non-invalid req_id with no bounds check (DF-1590's index aspect) and no busy-state/completer validation. Released requests keep stale cb pointers (set at tws_cam.c:738/829/875, cleared only on re-allocation at tws.c:812), so a duplicate/stale in-range id double-completes a surrendered ccb (second xpt_done at tws_cam.c:371 + callout_stop on dead ccb memory at :365) or early-completes a reallocated in-flight ccb (biodone of live bio, DF-4178 class). tws_get_param submits reqs[3] with cb=NULL (tws_cam.c:924) and polls (:942) β€” an interrupt delivering that response first is a NULL function call (panic); latent today only because get_param runs at attach with interrupts masked (tws.c:547, tws_hdm.c:112-114). The drain loop also terminates only when the firmware stops producing responses. Guest lacks the 3ware HBA, so not exercisable.