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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4590 Β· 11 files
FileTypeDescriptionSize
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.ko while 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.

VERDICT.md
↓ download raw

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

Fix 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
↓ fix.diffper-fix-DF-4590

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).