tws_detach/attach-unwind never cancel driver callouts (reset_cb, reinit poll, per-req timeouts) before freeing the softc β use-after-free panic at driver unload during reset recovery
Summary
The reinit poll self-re-arms every 5s while !ctlr_ready (cycles forever on a wedged controller), reset_cb arms at hz/10, and 60s per-request timeouts exist - but detach tears down the intr then kfrees reqs/sense_bufs/aen_q/trace_q with zero callout cancellation. A pending callout firing after the frees executes on freed heap (and with the KLD build, potentially unmapped module text). Realistic sequence: controller wedge (reinit cycling) + kldunload to re-probe -> softirq UAF within <=5s. Hardware-gated; defect certain from source. Cancel-all-callouts + init-every-thandle fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4590 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 3.5 KB | β raw | |
| trigger.sh | β | 2.5 KB | view raw | |
| run.sh | β | 486 B | view raw | |
| run.log | β | 537 B | view raw | |
| build.log | β | 212 B | view raw | |
| env.txt | β | 209 B | view raw | |
| guest_probe.log | β | 677 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.5 KB | view raw |
DF-4590 β tws(4) detach/unload never cancels driver callouts β use-after-free
Finding file: sys/dev/raid/tws/tws.c (pass 2) Lines: 346-410 (tws_detach), 273-276 (callout_init), 398-406 (frees); consumers tws_cam.c:847/893 (req->thandle), 1217/1232 (reset_cb), 1238/1256 (reinit).
What the bug is
tws_attach initializes three softc callouts (tws.c:274-276). During error recovery the driver schedules them:
sc->reset_cb_handleβ tws_reset() arms a hz/10 poll (tws_cam.c:1217); tws_reset_cb re-arms itself (1232) until the soft reset completes.sc->reinit_handleβ tws_reset_cb arms a 5 s reinit poll (1238); tws_reinit re-arms itself every 5 s while the controller is not ready (1256) β with a wedged controller this cycles indefinitely (and can even re-assert soft resets).req->thandleβ 60 s AEN-fetch / get-set-param timeouts (847, 893).
tws_detach() (tws.c:346-410) tears down the interrupt (370-374), then
kfree()s sc->reqs (398), sc->sense_bufs (399), the AEN/trace queues
(401-402), lockuninit()s the locks (403-406) and destroys the cdev (407) β
without a single callout_stop/callout_cancel. A callout that is armed
at detach time fires after those frees: tws_reinit/tws_reset_cb/tws_timeout
dereference sc (and sc->reqs[i]) from freed heap β use-after-free in
softirq context (panic at best, slab corruption at worst). The
attach-failure unwinds (309-341) have the same hole.
Preconditions / reachability
- tws is built into X86_64_GENERIC (sys/config/X86_64_GENERIC:125), so the
classic trigger is a module configuration (sys/dev/raid/tws/Makefile
builds KMOD tws) and root
kldunload tws.kowhile a reset/reinit poll or a 60 s request timeout is pending β i.e. exactly when an operator unloads the driver to recover a wedged 9700-series card (the scenario DF-1924/DF-1925 describe). Requires a LSI 3ware 13C1:1010 controller. - Privilege: root (kldunload). Hence severity Low, but the defect class is a kernel UAF (memcorrupt).
Reproduce (hardware required)
See trigger.sh for the exact recipe. EXPECTED vulnerable behavior: kldunload followed within β€ 5 s by a kernel panic (page fault in softclock/tws_reinit) or heap corruption. Patched: clean unload.
On the audit guest: no 3ware controller exists (pciconf shows no 0x13C1
device; dev.tws absent; tws compiled into the kernel and attached to
nothing β run.log / env.txt), so the trigger cannot run: verdict
not_reproduced, defect certain from source.
Build / run on guest
./run.sh # probes guest hardware; records absence in run.log
Fix
fix.diff: callout_cancel() the three softc callouts + all per-request thandles in tws_detach after IRQ teardown (before the kfrees), and callout_init() every req->thandle in tws_init_reqs so cancelling a never-used request is safe.
DF-4590 VERDICT β not_reproduced (hardware-gated), defect certain from source
Finding
tws_detach() (sys/dev/raid/tws/tws.c:346-410) frees the softc's dynamic
state β kfree(sc->reqs) at :398, kfree(sc->sense_bufs) :399,
xpt_free_ccb :400, kfree(sc->aen_q.q) :401, kfree(sc->trace_q.q)
:402, lockuninit() :403-406 β while driver callouts that reference
exactly that memory remain armed. Nothing between :346 and :410 calls
callout_stop/callout_cancel on:
sc->reset_cb_handleβ armed at tws_cam.c:1217, re-armed at :1232,sc->reinit_handleβ armed at tws_cam.c:1238, self-re-arming every 5 s at :1256 while!tws_ctlr_ready(sc)(i.e. forever on a wedged controller),sc->reqs[i].thandleβ 60 s timeouts armed at tws_cam.c:847, :893 (callout_init happens in tws_get_request, tws.c:805).
The attach-failure unwinds (tws.c:309-341) have the identical hole.
A pending callout executing after detach fires tws_reinit() /
tws_reset_cb() / tws_timeout() on freed heap and freed lock objects β
use-after-free in softirq context (page-fault panic or silent slab
corruption). With a module build (sys/dev/raid/tws/Makefile, KMOD=tws)
the module text itself is also freed, so the callout target may be
unmapped.
Why it is real and when it fires
The realistic operator sequence is: controller wedges (tws_reset runs,
"Resetting controller" on console, reinit poll cycles every 5 s β the
recovery path DF-1924/DF-1925 already documented as hang-prone) β operator
unloads the driver to re-probe β kldunload tws β detach frees everything
β β€5 s later the reinit poll fires into freed memory. Root-privileged,
hardware-dependent β hence severity Low β but the defect class is a
kernel UAF (memcorrupt bucket).
Reproduction attempt on the audit guest
- Guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (env.txt).
- run.sh / run.log / guest_probe.log: no LSI 3ware (0x13C1) PCI device;
dev.twsabsent; tws compiled into the kernel and attached to nothing. QEMU has no 3ware 9700 emulation, so the trigger cannot execute: status not_reproduced, reproduced=0, impact none (not manifested here). Realistic impact ceiling on real hardware: UAF panic / heap corruption at driver unload (privileged trigger).
Confidence: certain β the missing cancellation is directly visible in the teardown function; every callout consumer pointer is softc/reqs memory that the same function frees first.
Fix validation
fix.diff: (1) callout_cancel() all three softc callouts and every
reqs[i].thandle in tws_detach after bus_setup_intr teardown /
pci_release_msi and before tws_cam_detach/frees; (2)
callout_init(&sc->reqs[i].thandle) in tws_init_reqs so cancelling
never-handed-out requests is safe. git apply --check passes. Baseline
vs patched cannot be exercised on this guest (no 3ware HW) β
fix_status: not_testable; validated by inspection against tws.c:346-410,
597-636 and tws_cam.c:1217-1256.
Artifacts
- trigger.sh β exact unload-during-recovery recipe
- run.sh, run.log, guest_probe.log, env.txt β guest hardware-absence proof
- fix.diff β callout cancellation (git-apply clean)
- manifest.json / verdict.json
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 not validated on guest: baseline cannot run (no 3ware controller, no QEMU emulation). fix.diff is git-apply clean against tws.c:346-410/597-636 and cancels every callout the recovery paths arm; validated by inspection.
findings/poc/DF-4590/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (callout-consumer vs free-site line map); trigger.sh (exact unload-during-recovery recipe); run.log/env.txt/guest_probe.log (no 3ware HW on guest: pciconf empty for 13c1, dev.tws absent, kldload reports in-kernel); fix.diff (callout_cancel of 3 softc callouts + all reqs[i].thandle in detach + callout_init of thandles in tws_init_reqs; git-apply clean)
PoC changes
No seed pack existed; trigger authored as documented unload-during-recovery recipe plus guest hardware-absence probe.
Verified recommended fix
callout_cancel() the reset/reinit/print-stats callouts and every reqs[i].thandle in tws_detach (and attach-failure unwinds) after IRQ teardown, and callout_init() all thandles in tws_init_reqs so cancellation is always safe (see fix.diff).
Verdict
tws_detach() (tws.c:346-410) frees sc->reqs (398), sense_bufs (399), the AEN/trace queues (401-402) and lockuninit()s (403-406) while driver callouts referencing exactly that memory stay armed: sc->reset_cb_handle (tws_cam.c:1217/1232, hz/10 poll), sc->reinit_handle (tws_cam.c:1238/1256, self-re-arming every 5 s while the controller is not ready), and per-request thandle 60 s timeouts (tws_cam.c:847/893). No callout_stop/callout_cancel exists anywhere in the detach or attach-failure unwinds. A pending callout firing after detach executes tws_reinit/tws_reset_cb/tws_timeout on freed heap (and, with the module build that sys/dev/raid/tws/Makefile provides, potentially unmapped module text) - use-after-free in softirq context, panic or slab corruption. Not reproduced on the audit guest: no LSI 3ware (0x13C1) PCI device exists, tws is compiled into X86_64_GENERIC and attached to nothing, and QEMU cannot emulate a 9700-series controller (run.log/env.txt/guest_probe.log). Defect certain from source: every consumer pointer is freed by the same function that fails to cancel it. Trigger is root-privileged (kldunload during pending reset/reinit) - severity Low, class memcorrupt (UAF).
No comments yet.