Use-after-free in target teardown: fixed 500ms sleep before kfree(softc) does not drain in-flight CTIOs
Summary
targbhdtor() frees softc after fixed hz/2 sleep with no wakeup signaling while CTIOs dispatched by targbhstart() are neither tracked nor aborted by targbhdislun(). A CTIO still owned by the SIM when timeout expires completes into targbhdone(XPT_CONT_TARGET_IO) which dereferences freed softc (TAILQ_REMOVE on softc->pending_queue line 648 desc->data_resid write line 676) and freed periph. targbhdislun() only XPT_ABORTs idle accept_tio_list and immed_notify_slist - never aborts CTIOs dispatched to SIM. XXX comments at lines 342 and 419 acknowledge gap. Attacker as SCSI initiator can send INQUIRY then stall bus to keep CTIO pending beyond 500ms then trigger deregistration (bus reset camcontrol action hot-removal) to fire targbhdtor. Impact: kernel heap corruption groomable to arbitrary code execution minimally panic/DoS. [VERIFIED pass2: reproduced - verbatim driver held-CTIO teardown frees softc after fixed 65 ticks with no wakeup source, late completion 699 ticks later panics at targbhdone+0x439 Bad link elm on the deterministically reclaimed same-chunk canary victim; controls clean; production kernels get silent offset-deterministic write-after-free; root-gated reach - optional targbh absent from X86_64_GENERIC, no unpriv path so Medium stands; fix-validated active_ctios drain - no panic, zero post-free actions, canary intact]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2285 Β· 26 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 6.1 KB | β raw | |
| VERDICT.md | β | 12.1 KB | β raw | |
| verdict.json | β | 7.4 KB | view raw | |
| manifest.json | β | 2.3 KB | view raw | |
| fix.diff | β | 1.7 KB | view raw | |
| env.txt | β | 229 B | view raw | |
| patched_env.txt | β | 215 B | view raw | |
| build.log | β | 5.6 MB | β download | |
| installkernel.log | β | 80.4 KB | view raw | |
| fix_gen.log | β | 480 B | view raw | |
| fix_build.log | β | 5.6 MB | β download | |
| fix_installkernel.log | β | 80.4 KB | view raw | |
| run.log | β | 14.7 KB | view raw | |
| panic.txt | β | 1.8 KB | view raw | |
| panic.serial.log | β | 1.8 KB | view raw | |
| fix_run.log | β | 27.7 KB | view raw | |
| rig/df_pre.c | β | 5.1 KB | view raw | |
| rig/df_post.c | β | 23.0 KB | view raw | |
| rig/gen.sh | β | 1.3 KB | view raw | |
| rig/check_verbatim.sh | β | 749 B | view raw | |
| rig/build.sh | β | 2.3 KB | view raw | |
| rig/run.sh | β | 1.7 KB | view raw | |
| rig/run_fixed.sh | β | 1.4 KB | view raw | |
| rig/make_fix.sh | β | 1004 B | view raw | |
| rig/run_reclaim.sh | β | 529 B | view raw | |
| rig/run_panic.sh | β | 1.1 KB | view raw |
DF-2285 β scsi_targ_bh.c teardown UAF β evidence pack
Finding: Use-after-free in target teardown β targbhdtor() frees
softc after a fixed hz/2 (500 ms) sleep with no wakeup protocol,
while CTIOs dispatched by targbhstart() are neither tracked nor
aborted (targbhdislun() aborts only idle ATIOs / immediate notifies).
A CTIO still owned by the SIM completes into targbhdone() which
dereferences the freed softc (TAILQ_REMOVE on pending_queue at
scsi_targ_bh.c:648 β read AND write through &softc->pending_queue β
plus softc->state read at :678 steering the re-arm branch at :689).
Verdict (short)
REPRODUCED on the stock (INVARIANTS) guest kernel, mechanically, with
the verbatim driver code compiled into the kernel: teardown with a
held CTIO frees the softc after exactly the fixed 500 ms, the late
CTIO completion then executes targbhdone against the freed (and
same-chunk-reclaimed, 0x41-canary-filled) softc and the kernel panics
with Bad link elm ... prev->next != elm from inside targbhdone+0x439
(the INVARIANTS QMD_TAILQ_CHECK_PREV gate catching the
use-after-free read at scsi_targ_bh.c:648). Controls (no CTIO in
flight; CTIO completed before teardown) are clean. The A/B fixed
kernel (fix.diff: outstanding-CTIO counter + wakeup drain) blocks the
dtor until the CTIO completes, runs the completion with the softc
alive, and shows ZERO post-free driver actions with the canary fully
intact β no panic.
Reachability is root-gated (see VERDICT.md): scsi_targ_bh.c is
optional targbh (sys/conf/files:46), absent from X86_64_GENERIC, no
/boot/kernel/targbh.ko on the stock guest, and requires a
target-mode-capable SIM (PIT_PROCESSOR) β so no unprivileged path;
impact is a demonstrated kernel UAF/panic primitive in
root-configured target-mode deployments (CVSS AV:A/AC:H).
Rig
The established CAM verbatim-interposition kernel-compiled harness (same pattern as the DF-2270/scsi_ch rig):
rig/df_pre.cβ preprocessor-interposes every external the verbatim driver calls (xpt_action,xpt_alloc_ccb,xpt_setup_ccb,kmalloc/kfree, ...). Nothing in the driver is edited.- verbatim
sys/bus/cam/scsi/scsi_targ_bh.c(sha256-gated byrig/check_verbatim.sh; audit-tree hash4bb4bca6b3a2b0eb84a737d2251752f4c3ebd0c8d5e7c16ffb7e99411e49307b; fixed-build hash4103058df2c3bdccecb87a5ee57251b1008a0cc0ffb1b6d511a5d2fe7de97fc3). rig/df_post.cβ implements the shims: fabricated periph / path / sim (with a REAL lockmgr sim-lock so the REALsim_lock_sleep()intargbhdtorexecutes), a fake target-mode SIM that parks resource ATIOs/INs, holds a dispatched CTIO indefinitely (stalled initiator), aborts idle CCBs, and refuses to abort a SELECTED ATIO it no longer owns; and a dtor thread pinned to the ctor CPU that, immediately aftertargbhdtor()returns, reclaims the just-freed softc chunk with a 0x41 canary victim (same per-CPU slab zone, deterministic LIFO) so post-free driver touches are visible and any write lands in a live replacement object. Note: an earlier harness revision poisoned freed chunks withmemset(0xDE)afterkfree; DragonFly chains free chunks through the chunk itself (kern_slaballoc.c), so that scribble is a harness artifact (delayedchunk_mark_freeassertion) and was removed β post-free evidence is the canary victim + the panic backtrace.rig/gen.shconcatenates pre + verbatim + post intobus/cam/scsi/dftbh_harness.cand registers it as astandardkernel file (device targbhis NOT in X86_64_GENERIC, so the verbatim TU is the only compiled instance).rig/build.shbuilds/installs/reboots X86_64_GENERIC on the guest (INVARIANTS kernel,opt_global.h: #define INVARIANTS 1).
Commands on the guest: sysctl debug.dftbh.cmd=<ctor|inquiry|dtor|
dtorwait|complete|reclaim|periphfree|dump|status|abortmode=N>.
Build & run
# from findings/poc/DF-2285/rig on the audit host, guest up: ./build.sh # full log -> build.log (guest side: /root/build.log) ./run.sh # S0, S1 controls + S2 MAIN -> ../run.log + ../panic.txt # A/B fix validation: # (reset guest to with-src, re-push rig, apply ../fix.diff) ./run_fixed.sh # same scenarios on fixed kernel -> ../fix_run.log
Observed (baseline, stock INVARIANTS kernel)
- S0 CONTROL-1 (no I/O): teardown aborts 8 idle ATIOs + 8 INs, sleeps
65 ticks,
kfree(softc)β canary victim intact, no panic. - S1 CONTROL-2 (CTIO completed BEFORE teardown): normal re-arm, then clean teardown β no panic.
- S2 MAIN: CTIO held at the fake SIM;
targbhdtorreturns after the fixed 65 ticks with the CTIO STILL HELD (kfree(softc=... size=280) (CTIO still HELD at fake SIM)); the freed chunk is reclaimed same-CPU as a 0x41 canary victim (SAME CHUNK); pre-completion dump: fully 0x41; the completion fires 699 ticks after dispatch (>> hz/2=50, attacker-controlled) and the kernel panics:panic: Bad link elm 0xfffff8008db00cc0 prev->next != elmwith backtracetargbhdone+0x439β panic.txt.
Observed (fixed kernel, fix.diff applied)
- S2 MAIN: dtor thread blocks in the
targbhctiodrain; the completion runs withsoftc_freed=0(TEARDOWN branch, no re-arm, desc/atio freed cleanly); dtor returns after 192 ticks (drained); post-free canary dump:damaged_qwords=0,postfree=0,rearm=0β no panic. - S0/S1 controls bit-identical to baseline (65/66 ticks, clean).
Files
rig/β harness sources + gen/build/run scripts (see above)fix.diffβ maintainer-shaped fix (outstanding-CTIO counter + wakeup drain in targbhdtor; increment in targbhstart before xpt_action; decrement+wakeup in targbhdone CTIO completion)build.log,installkernel.logβ baseline build/install (host side)fix_build.log,fix_installkernel.log,fix_gen.logβ fixed buildrun.logβ full untrimmed baseline scenario dmesg (S0/S1/S2)panic.txt/panic.serial.logβ baseline S2 panic (serial console)fix_run.logβ full untrimmed fixed-kernel scenario dmesgenv.txt,patched_env.txtβ guest environment (baseline/fixed)VERDICT.md,manifest.json,verdict.jsonβ verdict + machine record
DF-2285 β scsi_targ_bh teardown UAF β VERDICT
Status: REPRODUCED (mechanical, verbatim driver code, stock INVARIANTS guest kernel). Impact: kernel use-after-free confirmed as a live primitive; observable as a deterministic kernel panic (DoS) with a groomable, attacker-window-controlled write/read-after-free core. Reachability is root-gated (no unprivileged path on the stock guest), so this closes as Medium β consistent with the filed severity.
1. Line-accurate teardown-path analysis (what targbhdtor actually does)
targbhdtor (sys/bus/cam/scsi/scsi_targ_bh.c:402-424):
softc->state = TARGBH_STATE_TEARDOWN(:409).targbhdislun(periph)(:411) which, in order: - walkssoftc->accept_tio_list(:347-354) issuingXPT_ABORTfor every ATIO the driver ever armed (targbhenlun:267-286). NOTE:accept_tio_listis only a resource inventory β a SELECTED ATIO (handed by the SIM to the periph with a CDB, its CTIO dispatched by targbhstart) is still linked there, but the abort is sent to the SIM for a ccb the SIM no longer owns; - walkssoftc->immed_notify_slist(:356-363) aborting the INs; - disables the LUN viaXPT_EN_LUN(enable=0)(:368-371). Nothing in the driver enumerates, aborts, or drains CTIOs (XPT_CONT_TARGET_IO) submitted bytargbhstartviaxpt_action(start_ccb)(:497). The author left two XXX markers:XXX Block for Continue I/O completion(:342) andXXX Wait for callback of targbhdislun()(:419).- falls through to
sim_lock_sleep(softc, 0, "targbh", hz/2, periph->sim->lock)(:420) β a fixed 500 ms timed sleep with no wakeup source (a tree-wide grep shows the onlywakeupcalls in the file are at :444 and :526, neither targetssoftc), thenkfree(softc, M_SCSIBH)(:421).
After the dtor returns, camperiphfree (sys/bus/cam/cam_periph.c,
camperiphfree) frees periph->path and periph itself β so a late
SIM completion that still remembers cbfcnp = targbhdone and the
periph runs entirely on freed memory (in the real kernel xpt_done
would deref the freed path even before targbhdone).
targbhdone (XPT_CONT_TARGET_IO case, :638-695) on a late completion
executes, against the freed softc: softc = periph->softc (:522),
TAILQ_REMOVE(&softc->pending_queue, &atio->ccb_h, ...) (:648) β
which READS *atio->tqe_prev (a pointer into the freed chunk) and
WRITES two qwords through/into the freed chunk (queue.h TAILQ_REMOVE
semantics) β desc->data_resid -= desc->data_increment (:676), the
softc->state read (:678) steering the xpt_action((union ccb*)atio)
re-arm (:689) versus the cleanup branch (:692-693).
2. Mechanical demonstration (verbatim driver compiled into the kernel)
Rig: pre/post interposition harness around a byte-for-byte verified
copy of scsi_targ_bh.c (sha256
4bb4bca6b3a2b0eb84a737d2251752f4c3ebd0c8d5e7c16ffb7e99411e49307b,
check_verbatim.sh: VERBATIM CHECK: embedded copy == sys source),
fabricated periph/path/sim with a REAL lockmgr sim-lock so the REAL
sim_lock_sleep runs; fake SIM parks ATIO/IN resources, holds the
dispatched CTIO (stalled initiator), aborts idle CCBs, cannot abort a
SELECTED ATIO (CAM_REQ_CMP_ERR β conservative SIM behaviour); the dtor
thread is CPU-pinned to the ctor so the just-freed softc chunk is
deterministically reclaimed as a 0x41 canary victim immediately after
targbhdtor returns.
Guest: DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC with
opt_global.h: #define INVARIANTS 1 (stock), harness kernel builds
1 (baseline) and #1 03:32:40 (fixed).
S2 MAIN (baseline) β the UAF
DFH: sim: CTIO 0xfffff801183d23c0 ACCEPTED AND HELD at fake SIM (stalled initiator; ticks=...) DFH: dtor thread: calling targbhdtor DFH: sim: XPT_ABORT idle ATIO ... -> completed CAM_REQ_ABORTED (x7) DFH: sim: XPT_ABORT SELECTED ATIO 0xfffff8008db00cc0 -> CAM_REQ_CMP_ERR (SIM cannot abort a ccb owned by the periph; in-flight CTIO untracked) DFH: sim: XPT_ABORT IN ... -> completed CAM_REQ_ABORTED (x8) DFH: sim: XPT_EN_LUN enable=0 DFH: kfree(softc=0xfffff8004f296c20 size=280) (CTIO still HELD at fake SIM) <-- free with outstanding CTIO DFH: dtor thread: targbhdtor RETURNED after 65 ticks (hz/2=50) <-- fixed 500ms, no wakeup protocol DFH: reclaim: canary victim 0xfffff8004f296c20 (0x41-filled, size 280) == freed softc chunk (SAME CHUNK reclaimed) DFH: dump[cmd] damaged_qwords(out of 35)=0 <-- nothing touched it yet DFH: complete: firing held CTIO 0xfffff801183d23c0 into targbhdone (outstanding 699 ticks, hz/2=50) softc_freed=1 panic: Bad link elm 0xfffff8008db00cc0 prev->next != elm targbhdone() at targbhdone+0x439
The panic is QMD_TAILQ_CHECK_PREV (sys/sys/queue.h:528-534, enabled
by _KERNEL && INVARIANTS) firing inside the verbatim TAILQ_REMOVE
at scsi_targ_bh.c:648: atio->tqe_prev points into the freed softc
chunk β now the live 0x41 canary victim β and the read of
*(atio->tqe_prev) returned 0x4141414141414141 != the elm. That is
a driver dereference of freed-then-reclaimed heap memory, caught by
the INVARIANTS gate. On a production (non-INVARIANTS) kernel the same
code proceeds to WRITE through those pointers (TAILQ_REMOVE writes
*tqe_prev = tqe_next and head->tqh_last = tqe_prev, queue.h:646-663)
β i.e. an offset-deterministic write-after-free (NULL + self-pointer
into whatever object now occupies the chunk) plus the
softc->state-from-freed-memory control-flow read at :678 that would
take the re-arm branch (:689) and push the stale ATIO back into a dead
periph. The timing is fully attacker-controlled: the held CTIO was
outstanding 699 ticks (6.99 s) versus the fixed 500 ms budget β the
initiator simply keeps stalling the DATA phase while teardown runs
(hot-remove / camcontrol / SIM dereg).
Controls (baseline)
- S0 CONTROL-1 (ctor -> dtor, no I/O): aborts 8 ATIOs + 8 INs,
sleeps 66 ticks, frees softc; canary victim intact
(
damaged_qwords=0); no panic. - S1 CONTROL-2 (ctor -> inquiry -> complete BEFORE dtor): the CTIO
completes while the softc is alive (
softc_freed=0,postfree_actions=0), the ATIO re-arms normally, teardown is then clean; no panic.
So the panic appears exactly and only when a dispatched CTIO is still outstanding at teardown β the precise condition the finding claims.
Harness-artifact disclosure
An intermediate harness revision scribbled 0xDE over freed chunks to
make UAF touches visible; DragonFly chains free chunks through the
chunk itself (kern_slaballoc.c z_LChunks/c_Next), so that scribble
delay-panicked chunk_mark_free in slab_cleanup β a harness bug,
not the finding. It was replaced by the same-chunk canary victim (a
LIVE allocation; structurally safe) before the decisive runs recorded
here. No other harness-induced anomalies.
3. A/B fix validation (fix.diff)
fix.diff ports the maintainer-shaped teardown discipline:
u_int active_ctios in the softc, incremented in targbhstart
BEFORE xpt_action(start_ccb) (the SIM may complete synchronously),
decremented + wakeup(&softc->active_ctios) in the targbhdone
XPT_CONT_TARGET_IO case, and targbhdtor now drains
(while (softc->active_ctios > 0) sim_lock_sleep(&softc->active_ctios,
0, "targbhctio", 0, periph->sim->lock)) before the existing hz/2
sleep and kfree. Divergence note: FreeBSD CURRENT still carries the
same fixed hz/2 sleep + free in its scsi_targ_bh.c (their
cam_periph_sleep(periph, softc, PRIBIO, "targbh", hz/2)), i.e. the
defect is inherited from the 2003 FreeBSD base and persists upstream;
FreeBSD scsi_target.c (targ(4)) by contrast frees queued CCBs and
clears periph->softc in its dtor.
Fixed-kernel observations (fix_run.log):
DFH: complete: firing held CTIO ... (outstanding 269 ticks, hz/2=50) softc_freed=0 <-- completion now lands BEFORE the free DFH: complete: targbhdone returned; postfree_actions=0 rearm_after_free=0 DFH: kfree(softc=0xfffff8004f296c20 size=288) (CTIO still not held) DFH: dtor thread: targbhdtor RETURNED after 192 ticks (hz/2=50) <-- dtor BLOCKED in the drain, then freed DFH: dump[cmd] damaged_qwords(out of 36)=0 <-- canary fully intact after free status: ... ctio_held=0 softc_freed=1 postfree=0 rearm=0 victim=1
- S2 MAIN on the fixed kernel: no panic, zero post-free driver actions, canary intact, the TEARDOWN branch (softc alive) frees the desc/atio cleanly, and the dtor demonstrably WAITED for the CTIO (192/319 ticks vs the fixed 65 in baseline).
- S0/S1 controls: bit-identical to baseline (65/66 ticks, clean) β the normal path is unchanged apart from the counter bookkeeping.
4. Threat model / reachability (honest)
sys/conf/files:46βbus/cam/scsi/scsi_targ_bh.c optional targbh; NOT in X86_64_GENERIC; notargbhsymbols in the stock kernel (nm /boot/kernel/kernel), no/boot/kernel/targbh.koon the guest. Reaching the driver requires the root admin to build/load it (device targbh/ kldload) AND a target-mode-capable SIM (PIT_PROCESSOR, targbhasync:203) β none of the QEMU-emulated HBAs expose that in DragonFly. So: no unprivileged reachability on the stock system; no uid0 chain exists to demonstrate. Per protocol (root-gated), this closes with the primitive characterization.- In a root-configured target-mode deployment the trigger divides as: the remote initiator keeps one CTIO in flight (stall the DATA phase), then teardown happens (SIM dereg / hot-remove / admin). The 500 ms budget is attacker-influenced only insofar as the initiator controls the stall β the window is trivially > 500 ms.
- Primitive: offset-deterministic read+write-after-free on a 280-byte
M_SCSIBH chunk (pending_queue head at +0/+8; state at +248),
groomable (same-chunk canary reclaim demonstrated; M_SCSIBH is a
dedicated type with a small, driver-controllable population),
followed by use of the freed
periph(camperiphfree frees it right after the dtor). On INVARIANTS kernels this is a reliable panic (remote-triggerable DoS from the initiator position); on production kernels it is silent cross-object corruption. The write values through TAILQ_REMOVE are NULL/self-pointer (not directly arbitrary), but the re-arm branch (:689) then re-submits a stale ATIO whose descriptor was freed in the teardown branch of a concurrent completion β additional use/double-free surface. Full code-execution escalation was NOT developed: reachability is root-gated, which caps the severity at Medium (DoS/corruption).
5. Residual notes
- abortmode=2 (a SIM that would complete an abort for a ccb it already handed to the periph) is a theoretical amplifier not exercised by the verdict runs; the conservative SIM behaviour (abort refused) is what any conforming SIM does for a ccb it does not own.
- The harness
periphfreecommand modelscamperiphfreepoisoning the periph/path after the dtor (S4 amplification); it was not needed for the verdict since the in-driver panic already demonstrates the UAF at :648. - Baseline guest-kernel build log was overwritten by the fixed build
on the guest (
/root/build.log); the baseline build artifacts in this pack are build.log (host-side driver of the same build), installkernel.log, and the booted-kernel evidence in run.log. The fixed build kept its full 35k-line log (fix_build.log).
Bottom line
The claim is verified end-to-end on the verbatim driver: the fixed 500 ms sleep is the ONLY teardown barrier, dispatched CTIOs are invisible to it, the softc (and in the real kernel the periph and path) are freed while a CTIO is still SIM-owned, and the late completion dereferences the freed chunk β panicking the stock INVARIANTS kernel inside targbhdone+0x439 with the chunk reclaimed by a canary victim. The four-line-counter fix eliminates it (drain before free), with controls unchanged.
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_testableConfirmed kernel references
- sys/bus/cam/scsi/scsi_targ_bh.c:402
- sys/bus/cam/scsi/scsi_targ_bh.c:420
- sys/bus/cam/scsi/scsi_targ_bh.c:421
- sys/bus/cam/scsi/scsi_targ_bh.c:329
- sys/bus/cam/scsi/scsi_targ_bh.c:342
- sys/bus/cam/scsi/scsi_targ_bh.c:347
- sys/bus/cam/scsi/scsi_targ_bh.c:419
- sys/bus/cam/scsi/scsi_targ_bh.c:497
- sys/bus/cam/scsi/scsi_targ_bh.c:522
- sys/bus/cam/scsi/scsi_targ_bh.c:648
- sys/bus/cam/scsi/scsi_targ_bh.c:676
- sys/bus/cam/scsi/scsi_targ_bh.c:678
- sys/bus/cam/scsi/scsi_targ_bh.c:689
- sys/bus/cam/cam_periph.c:626
- sys/sys/queue.h:528
- sys/kern/kern_slaballoc.c:815
- sys/conf/files:46
Detail
Exploit chain
remote initiator keeps one CTIO in flight (stall DATA phase past the 500 ms teardown budget) -> SIM/periph deregistration runs targbhdtor -> targbhdislun aborts only idle ATIOs/INs, never the in-flight CTIO -> kfree(softc) with the CTIO outstanding -> late CTIO completion into targbhdone -> TAILQ_REMOVE at :648 reads and writes through the freed chunk (on INVARIANTS: panic at the QMD_TAILQ_CHECK_PREV read; on production kernels: silent NULL+self-pointer writes into the replacement object) -> softc->state read from freed memory steers the re-arm branch at :689 which re-submits the stale ATIO into the torn-down periph -> camperiphfree has also freed periph and path. Escalation to uid0 was NOT developed: the driver is not reachable by unprivileged users on a stock system (root-gated module/config plus target-mode HBA), which caps severity at Medium; the memory-corruption primitive itself was demonstrated mechanically (same-chunk canary reclaim).
Evidence (decisive lines)
panic.txt: panic Bad link elm 0xfffff8008db00cc0 prev->next != elm with backtrace targbhdone+0x439 fired by the complete command with softc_freed=1. run.log: kfree(softc=0xfffff8004f296c20 size=280) (CTIO still HELD at fake SIM), targbhdtor RETURNED after 65 ticks (hz/2=50), reclaim: canary victim == freed softc chunk (SAME CHUNK reclaimed), dump damaged_qwords=0 pre-completion, complete: outstanding 699 ticks softc_freed=1; S0/S1 controls clean. fix_run.log: complete ... softc_freed=0, postfree_actions=0 rearm_after_free=0, dtor RETURNED after 192 ticks (drained), damaged_qwords=0, no panic; S0/S1 identical to baseline. rig/check_verbatim.sh output in build.log/fix_gen.log: embedded copy == sys source byte-for-byte (baseline 4bb4bca6..., fixed 4103058d...).
PoC changes
No pack existed (seed was the finding text only). Built the CAM verbatim-interposition kernel-compiled harness per the established DF-2270 pattern: df_pre.c renames all externals the driver calls, df_post.c implements the fake target-mode SIM (holds dispatched CTIOs, refuses to abort a SELECTED ATIO), a CPU-pinned dtor thread running the real targbhdtor under a real sim lock, and a same-chunk 0x41 canary victim reclaimed immediately after kfree. Two harness iterations were required: (1) DFly kmalloc/kfree are macros in sys/malloc.h, so the interposition needed #undef first and the shims call _kmalloc/_kfree; (2) post-free 0xDE scribbling corrupted slab free-list linkage (harness artifact, delayed chunk_mark_free panic) and cross-CPU zones broke the reclaim, fixed by the canary-victim design with a CPU-pinned dtor thread.
Verified recommended fix
Track dispatched CTIOs with an outstanding counter and drain them before freeing: u_int active_ctios in targbh_softc, increment in targbhstart before xpt_action, decrement+wakeup in the targbhdone XPT_CONT_TARGET_IO case, and replace the bare hz/2 sleep in targbhdtor with while (softc->active_ctios > 0) sim_lock_sleep(&softc->active_ctios, 0, targbhctio, 0, periph->sim->lock); see fix.diff (A/B validated).
Verdict
REPRODUCED on the stock INVARIANTS guest kernel with the verbatim scsi_targ_bh.c compiled into the kernel via the CAM verbatim-interposition harness (sha256-gated). With a dispatched CTIO held by the fake SIM (stalled initiator), targbhdtor sleeps the fixed hz/2 (65 ticks, no wakeup source exists in the driver), kfrees the softc while the CTIO is still SIM-owned, the freed chunk is deterministically reclaimed as a 0x41 canary victim on the same per-CPU slab zone, and the late completion (699 ticks outstanding, attacker-controlled window) executes targbhdone against the freed softc: the TAILQ_REMOVE at scsi_targ_bh.c:648 dereferences the freed chunk and the kernel panics with Bad link elm prev->next != elm from targbhdone+0x439 (INVARIANTS QMD_TAILQ_CHECK_PREV). Controls (no in-flight CTIO; CTIO completed before teardown) are clean; the A/B fixed kernel (active_ctios counter + wakeup drain in targbhdtor) blocks the dtor until the CTIO completes, shows softc_freed=0 at completion, zero post-free actions, canary intact, and identical control behaviour. Reachability is root-gated: optional targbh in sys/conf/files:46, absent from X86_64_GENERIC, no targbh.ko, requires a PIT_PROCESSOR target-mode SIM; no unprivileged path exists, so the demonstrated primitive (offset-deterministic read/write-after-free on a groomable 280-byte M_SCSIBH chunk plus freed-periph use via camperiphfree) caps at Medium DoS/corruption in root-configured target-mode deployments.
No comments yet.