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

tws_scsi_err_complete touches the ccb after xpt_done (callout_stop on a surrendered ccb) β€” CAM ccb use-after-free race on every error completion

Summary

On error completions the driver calls xpt_done(ccb) and only then callout_stop(req->ccb_ptr->ccb_h.timeout_ch). xpt_done queues the ccb after which CAM may complete/recycle/free it; the later callout_stop reads freed memory or cancels an unrelated transaction's timeout. The sibling completion path uses the correct order, as does the drain path - the inversion is unique to this site. Unpriv I/O on check conditions -> UAF or silent hung-CAM. Swap-the-order fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4398 Β· 8 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 2.8 KB ↓ raw
trigger.c β€” 901 B view raw
build.sh β€” 82 B view raw
run.sh β€” 163 B view raw
fix.diff β€” 671 B view raw
verdict.json β€” 2.3 KB view raw
env.txt β€” 541 B view raw

DF-4398 β€” tws_scsi_err_complete touches the ccb AFTER xpt_done (callout_stop on a surrendered ccb)

What

sys/dev/raid/tws/tws_cam.c:593-596 β€” tws_scsi_err_complete() calls xpt_done(ccb) (surrendering the ccb to CAM: it is queued to sim_doneq and processed by the cambio SWI, sys/bus/cam/cam_xpt.c:4891-4926) and only THEN executes callout_stop(req->ccb_ptr->ccb_h.timeout_ch) β€” reading req->ccb_ptr->ccb_h.timeout_ch out of the ccb and stopping the callout it points at. Once xpt_done has queued the ccb, the peripheral's done routine (dadone et al.) may complete the transaction and recycle (xpt_release_ccb back to the periph pool) or free the ccb concurrently; the correct order β€” used by this very driver in tws_scsi_complete() (callout_stop at :365 BEFORE xpt_done at :371) and in tws_drain_busy_queue() (:627 before :634) β€” is to stop the callout first.

Impact

Race window between xpt_done(ccb) at :593 and callout_stop(...) at :596 on a error-completion path (tws_err_complete β†’ tws_scsi_err_complete, i.e. every controller error response / CHECK CONDITION): - callout_stop on freed ccb memory (use-after-free), or - callout_stop on a reused ccb whose timeout_ch now belongs to a NEW in-flight transaction β†’ the new I/O silently loses its timeout β†’ hung CAM transaction with no recovery (no tws_timeout β†’ no reset).

Trigger (needs a 3ware 9700/9700X controller)

Heavy concurrent disk I/O on a unit that returns check conditions (or any FW error completion), racing the cambio SWI. trigger.sh documents the workload.

Reproduce

./build.sh    # nothing to compile; workload is dd(1) based
./run.sh      # on a host with a tws(4) adapter and a failing/busy unit

Expected on vulnerable kernel: rare panic in callout/cam code or unkillable hung transaction after error completions under load.

Status on the audit QEMU guest

Guest has no 3ware HBA (pciconf shows no vendor 0x13c1, no /dev/tws*) β†’ cannot be exercised there; code-level proof in VERDICT.md. verdict: inconclusive.

VERDICT.md
↓ download raw

VERDICT β€” DF-4398

Status: inconclusive (hardware-gated) β€” the audit QEMU guest has no 3ware controller (vendor 0x13c1), so the path cannot be executed there. The code-level violation is certain; only the race exploitation needs the HBA.

Why this is a real bug (line-by-line)

  1. tws_err_complete() (tws_cam.c:519-530) dispatches TWS_SCSI_IO_REQ error completions to tws_scsi_err_complete(req, hdr).
  2. tws_scsi_err_complete() (tws_cam.c:549-603): c 591: ccb->ccb_h.status &= ~CAM_SIM_QUEUED; 592: lockmgr(&sc->sim_lock, LK_EXCLUSIVE); 593: xpt_done(ccb); 594: lockmgr(&sc->sim_lock, LK_RELEASE); 596: callout_stop(req->ccb_ptr->ccb_h.timeout_ch); /* AFTER xpt_done! */
  3. xpt_done() (sys/bus/cam/cam_xpt.c:4891-4926) queues the ccb on sim->sim_doneq and raises setsoftcambio(); the cambio SWI then runs the periph done routine (dadone β†’ biodone β†’ xpt_release_ccb) with NO reference held by the SIM. After :593 the driver no longer owns the ccb.
  4. Between :594 (sim_lock released) and :596 the SWI on another CPU can complete and recycle the ccb. req->ccb_ptr->ccb_h.timeout_ch is then (a) freed memory (periph teardown: xpt_free_ccb) or (b) the new owner's callout. callout_stop() on either is a UAF / spurious cancellation of an unrelated in-flight transaction's timeout.
  5. Contrast the correct order used by the same file: tws_scsi_complete() :365 (callout_stop) β†’ :371 (xpt_done), and tws_drain_busy_queue() :627 β†’ :634.

Trigger / threat

Any unprivileged disk I/O hitting a unit error (medium error, reset unit attention, reservation conflict β€” anything the firmware reports as an error response) while the cambio SWI is active. Ordinary load; no malicious input required beyond what makes a disk return errors β€” but it IS a pure race (window of a few instructions), so practical frequency is low per event, recurring across every error completion.

Exploit chain

Not developed β€” requires the physical HBA. Realistic ceiling: kernel memory corruption via callout_stop on freed/reused callout state (panic-class), or denial-of-service via silently un-timed CAM transactions.

Fix validation

Not testable without the controller (fix_status: not_testable); fix.diff moves callout_stop before the status mutation / xpt_done, matching tws_scsi_complete.

Environment

  • Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 (X86_64_GENERIC) x86_64, no 3ware HBA, no /dev/tws*, tws not loaded (see env.txt).

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 moves callout_stop() before the xpt_done() block, restoring the ownership invariant used by the other completion paths. Cannot be validated on the audit guest (no 3ware controller to build a baseline on).

fix.diff (order swap, no functional change to completion semantics)
↓ fix.diffper-fix-DF-4398

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (line-by-line ownership analysis); fix.diff (order swap); sys/dev/raid/tws/tws_cam.c:593-596 vs :365-371; sys/bus/cam/cam_xpt.c:4891-4926; env.txt (guest lacks controller)

PoC changes

no seed existed; trigger.c is a plain error-I/O hammer written for this pack

Verified recommended fix

Stop the ccb timeout callout before xpt_done() in tws_scsi_err_complete(), matching tws_scsi_complete().

Verdict

Code-level violation certain: tws_scsi_err_complete() executes callout_stop(req->ccb_ptr->ccb_h.timeout_ch) at sys/dev/raid/tws/tws_cam.c:596 AFTER xpt_done(ccb) at :593 surrendered the ccb to CAM (xpt_done queues to sim_doneq, sys/bus/cam/cam_xpt.c:4891-4926; the cambio SWI can recycle/free the ccb before :596 runs). The correct order (callout_stop before xpt_done) is used by tws_scsi_complete (:365/:371) and tws_drain_busy_queue (:627/:634) in the same file. Could not be exercised: the audit QEMU guest has no 3ware HBA (no vendor-0x13c1 device, no /dev/tws*).