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)
PoC verification
Evidence pack
findings/poc/DF-4398 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 β 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)
tws_err_complete()(tws_cam.c:519-530) dispatchesTWS_SCSI_IO_REQerror completions totws_scsi_err_complete(req, hdr).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! */xpt_done()(sys/bus/cam/cam_xpt.c:4891-4926) queues the ccb onsim->sim_doneqand raisessetsoftcambio(); 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.- Between :594 (sim_lock released) and :596 the SWI on another CPU can complete
and recycle the ccb.
req->ccb_ptr->ccb_h.timeout_chis 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. - Contrast the correct order used by the same file:
tws_scsi_complete():365 (callout_stop) β :371 (xpt_done), andtws_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_testablefix.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)
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*).
No comments yet.