cam_sim freed while still pending in the camisr completion softint β UAF write plus function-pointer dispatch from freed object (cam_sim_release has no synchronization against xpt_done/camisr)
Summary
The final kfree(sim) has no synchronization against the deferred-completion subsystem: xpt_done is lock-free by design and camisr pops the sim off the global cam_simq BEFORE taking the sim lock, so a detach thread holding the lock runs the entire in-tree deregister+free protocol while camisr is blocked on a raw sim pointer - then writes the freed chunk (flags RMW), lockmgr()s the freed embedded lock, walks the freed doneq, and dispatches (*ccb_h->cbfcnp)(): a kernel call through freed-object contents. Rig on production symbols: mode 1 UAF CONFIRMED (completion callback run out of a kfree'd cam_sim chunk, silent), mode 2 freed-chunk head -> fatal stop in camisr_runqueue (guest down), mode 4 baseline refcount-0-after-free. The interleaving is exactly the in-tree detach protocol (ahci/virtio_scsi/atapi-cam/mpr) + any softint delay. Pin-while-linked + cmpset-finalizer fix validated on a rebuilt kernel (refcount pin held, marker fires with sim valid).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5592 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ptsim3.c | β | 18.8 KB | view raw | |
| build.sh | β | 117 B | view raw | |
| run.sh | β | 169 B | view raw | |
| build.log | β | 1.3 KB | view raw | |
| run.log | β | 2.5 KB | view raw | |
| panic.txt | β | 16.1 KB | view raw | |
| fix.diff | β | 4.7 KB | view raw | |
| fix_build.log | β | 5.6 MB | β download | |
| fix_run.log | β | 2.7 KB | view raw | |
| README.md | β | 3.3 KB | β raw | |
| VERDICT.md | β | 6.1 KB | β raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 5.6 KB | view raw | |
| env.txt | β | 515 B | view raw |
DF-5592 β use-after-free: cam_sim freed while still pending in the camisr completion softint
sys/bus/cam/cam_sim.c pass-2 (GLM 5.3) β cam_sim_release() / cam_sim_free().
What
cam_sim_release() (cam_sim.c:241-256) performs the final kfree(sim) with no
synchronization against the CAM deferred-completion subsystem:
xpt_done()(cam_xpt.c:4891-4926) is lock-free by design β it inserts the completed ccb onsim->sim_donequndersim->sim_spinonly, then links the sim onto the globalcam_simqundercam_simq_spinand callssetsoftcambio(). This is the contract controller interrupt handlers rely on.swi_cambio β camisr()(cam_xpt.c:7389-7407) pops the sim offcam_simqundercam_simq_spinand only then takesCAM_SIM_LOCK(sim)β so a detach thread that already holds the sim lock runs its entirexpt_bus_deregister()+cam_sim_free()while camisr is blocked holding nothing but a rawstruct cam_sim *.- The standard driver detach protocol is exactly that: lock;
xpt_bus_deregister();cam_sim_free(); unlock β ahci_cam.c:210-227, virtio_scsi.c:675-687, atapi-cam.c:895-900, mpr_sas.c:955-956.
Result: kfree(sim) lands while the sim is still queued for / undergoing
completion processing. When the lock is released, camisr writes the freed
chunk (sim->flags &= ~CAM_SIM_ON_DONEQ at cam_xpt.c:7403 β note: plain
read-modify-write on freed memory), runs lockmgr() on the freed embedded
lock (7402), spin_lock(&sim->sim_spin) + walks sim->sim_doneq (7415-7417),
and dispatches (*ccb_h->cbfcnp)(path->periph, ccb) (7506) β a kernel
function-pointer call driven by freed-object contents.
Reproduce (guest)
sh build.sh # builds ptsim3/ptsim3.ko (mini-SIM rig) kldload ./ptsim3/ptsim3.ko sysctl debug.ptsim3.go=1 # mode 1: silent UAF proof (marker callback) sysctl debug.ptsim3.go=2 # mode 2: poison -> Fatal stop in camisr_runqueue sysctl debug.ptsim3.go=4 # mode 4: fix-validation trail
The rig (ptsim3.c) is a contract-clean mini-SIM chassis (the DF-5550 ptsim2
design): it registers a real bus with production symbols, parks probe ccbs as
"hardware", releases exactly one via the REAL xpt_done() from a kthread that
holds no sim lock (the interrupt-handler privilege), lets the swi block on the
sim lock the rig holds, then executes the ahci_cam_detach() sequence
verbatim. Expected:
- mode 1 (baseline kernel):
[PTSIM3] *** UAF CONFIRMED: kernel ran a completion callback out of a cam_sim chunk that cam_sim_free() had already kfree()'d ***βmarker_hit=1. - mode 2 (baseline kernel):
[Stopped at camisr_runqueue+0x53: movq 0x20(%r15),%rax]β DDB stop / Fatal trap 12 from the poisoned freed chunk. - mode 4 on the FIXED kernel:
cam_sim_free() returned; chunk refcount now reads 1(pin held β not freed), marker callback fires with the sim valid, chunk freed only after processing.
Fix
fix.diff β pin the sim (atomic ref) while it is linked on cam_simq; camisr
drops the pin after processing; cam_sim_release() becomes an atomic cmpset
protocol mirroring xpt_release_bus(); final 1β0 transition under
cam_simq_spin (xpt_sim_finalize); cam_dead_sim.refcount pinned high.
Validated by rebuild + re-run (see VERDICT.md).
DF-5592 VERDICT β REPRODUCED (use-after-free, deterministic on production symbols)
Bottom line
cam_sim_release() (sys/bus/cam/cam_sim.c:241-256) frees a struct cam_sim
that the CAM completion softint (swi_cambio β camisr, cam_xpt.c:7389-7407)
still holds a raw pointer to β and, in the second variant, that a lock-free
xpt_done() from interrupt context (cam_xpt.c:4901-4917) can still be
inserting into. Reproduced two ways on the stock guest kernel
(6.5-DEVELOPMENT X86_64_GENERIC) with a KLD rig that uses only production
symbols:
-
Silent UAF with control-flow proof (mode 1). A marker ccb was queued on the dying sim's
sim_doneqimmediately beforecam_sim_free()(exactly what a late interrupt-contextxpt_done()does at cam_xpt.c:4904-4907). After the free + lock release,camisr_runqueuepopped it and executed(*ccb_h->cbfcnp)(...)(cam_xpt.c:7506) β a kernel function-pointer call dispatched out of akfree()'d object.marker_hit=1; the guest kept running (silent corruption). -
Crash proof (mode 2). The freed chunk's
sim_doneq.tqh_firstwas set to0x00dead000(freed memory is still mapped β the standard post-UAF attacker write). After the lock release, camisr faulted:[Stopped at camisr_runqueue+0x53: movq 0x20(%r15),%rax]β guest in DDB, down. (panic.txt = serial console capture.)
The interleaving is 100% the in-tree detach protocol (ahci_cam.c:210-227:
lockmgr(ap_sim_lock); xpt_freeze_simq(); xpt_bus_deregister(); cam_sim_free();
lockmgr release): the rig merely makes the softint's arrival deterministic
(it blocks on the sim lock the detacher holds, which is precisely what happens
in-tree whenever the swi is scheduled after the detach thread acquires the
lock β e.g. under load, on a busy softint CPU, or kldunload during I/O).
Primitive / exploitation ceiling
- UAF write in softint context:
sim->flags &= ~CAM_SIM_ON_DONEQ(cam_xpt.c:7403) is a read-modify-write on the freed chunk (bit clear at offset offlags). lockmgr()on the freed embeddedstruct lock(7402) β lock-count/owner writes into freed memory.spin_lock(&sim->sim_spin)+ TAILQ walk of the freedsim_doneq(7415-17).- Function-pointer dispatch
(*ccb_h->cbfcnp)(path->periph, ccb)(7506): whoever reallocates the 168-byte M_CAMSIM chunk controlscbfcnp,path, and the whole ccb image β arbitrary kernel call with two controlled pointer args. On this guest (no SMAP/SMEP/KASLR) that is a straight code-execution primitive; the natural trigger class is controller detach (root kldunload / virtio device destroy / surprise removal) racing completion, so the attacker model is "root-adjacent or physical/hardware event + timing", not unprivileged-local. Impact recorded as panic (deterministic crash proven); the escalation chain is documented but not weaponized.
Why this is cam_sim.c's bug
The free decision lives in cam_sim_release(). It takes no lock, checks no
CAM_SIM_ON_DONEQ, never unlinks from cam_simq, and uses a non-atomic
if (refcount == 1) ... kfree protocol (the acquisition side,
xpt_bus_register(), uses atomic_add_int at cam_xpt.c:4322 β the asymmetry
itself). Every neighbor finalizer in cam_xpt.c
(xpt_release_bus 4979-5008, xpt_release_target 5048-β¦, xpt_release_device
5192-5232) uses atomic cmpset loops precisely because these objects die
concurrently; cam_sim.c predates that discipline.
Fix validation
fix.diff: (a)xpt_done()pins the sim (atomic_add_intundercam_simq_spin) when linking it oncam_simq; (b)camisr()clearsCAM_SIM_ON_DONEQfor popped sims under the same spin and drops the pin withcam_sim_release(sim, 0)only after processing+unlock; (c)cam_sim_release()rewritten as an atomic cmpset loop; the final 1β0 transition happens inxpt_sim_finalize()undercam_simq_spinso it cannot interleave with the pin acquisition; (d)cam_dead_sim.refcountpinned high (0x40000000) so the static dead sim can never be finalized.- Built as
make nativekernel KERNCONF=X86_64_GENERICon the guest (fix_build.log), installed, rebooted. - Mode 4 comparison (same rig, same choreography):
- baseline:
cam_sim_free() returned; chunk refcount now reads 0(already kfree'd while camisr blocked) andmarker_hit=1on freed memory. - fixed:
chunk refcount now reads 1(pin held β sim NOT freed),marker_hit=1with the sim valid+locked, and the chunk only reaches kfree after camisr drops the pin β the UAF window is gone; no trap. - (fix_run.log / serial capture.)
Attempt log
- First build: rig panicked in its own boot scan (
LK_CANRECURSEmissing) β rig bug, fixed. - First deregistration attempt with 8 attached da devices wedged inside xpt_bus_deregister teardown (destroy_dev under sim lock) β rig redesign: noattach mode (INQUIRY β CAM_REQ_INVALID), scan-on-demand, probe ccbs parked; teardown surface eliminated. (Mode-3 leak run then completed.)
- Mode 1 first run: race executed but heap-spray reclaim missed the chunk; replaced reclaim-marker with the marker-ccb callback (stronger proof, no allocator dependence) β reproduced.
- Mode 2: spray missed again; replaced with direct write of the freed chunk (post-UAF standard step) β camisr_runqueue fault, DDB stop.
- Fix build:
buildkernelrefused (no buildworld stamps on snapshot) βnativekernel(same as DF-5550's validation).
Cross-references
- The rig logs incidentally show
cam_devq_release: WARNING active allocations 8 active send 8!firing during the detach β the DF-5552 / DF-5594 class (devq dropped with active I/O), corroborating that provenance on a real deregistration flow. - DF-5593 (same run): the sim's ccb pool (
ccb_freeq, 8 ccbs) is orphaned bycam_sim_free()β measured leak, separate finding.
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
fixedfix.diff applied to guest /usr/src, kernel rebuilt (make nativekernel KERNCONF=X86_64_GENERIC, fix_build.log), installed, rebooted; the exact mode-4 rig choreography that baseline-crashed (mode 2) and freed the sim early (modes 1/4: chunk refcount 0 right after cam_sim_free) now shows chunk refcount 1 (cam_simq pin held -> sim NOT freed while camisr blocked), the completion callback runs with the sim valid+locked, and the chunk reaches kfree only after camisr drops the pin -- UAF window closed, no trap
fix.diff; fix_build.log; fix_run.log (patched-kernel mode-4 serial trail)
Confirmed kernel references
Detail
Exploit chain
detacher holds sim lock -> late xpt_done() (lock-free, interrupt context) links sim on cam_simq and arms swi -> camisr pops sim, blocks on sim lock -> detacher runs xpt_bus_deregister()+cam_sim_free() -> kfree(sim) while camisr holds raw pointer -> lock release -> camisr: lockmgr on freed lock, flags RMW write, spin_lock, walks freed sim_doneq, calls (*ccb_h->cbfcnp)(path->periph, ccb) with ccb image living in freed/reused memory -> attacker who reallocates the 168-byte M_CAMSIM chunk controls the call target and both pointer args (arbitrary kernel call; retpoline-thunked but resolvable on this guest). PoC chain executed up to the controlled-callback stage via marker ccb (cbfcnp invoked from freed sim) and to a hard fault via poisoned doneq head.
Evidence (decisive lines)
run.log (decisive serial lines incl. '*** UAF CONFIRMED: kernel ran a completion callback out of a cam_sim chunk that cam_sim_free() had already kfree()'d ***'); panic.txt (full console: [Stopped at camisr_runqueue+0x53: movq 0x20(%r15),%rax] db>); ptsim3.c (rig; modes 1/2/4); VERDICT.md; fix.diff + fix_build.log + fix_run.log (fix validation); mode 4 baseline trail shows chunk refcount 0 (freed) immediately after cam_sim_free while camisr still blocked
PoC changes
Original sketch did not exist (new finding); rig chassis derived from DF-5550's ptsim2 with: LK_CANRECURSE sim lock (rig boot panic otherwise), noattach INQUIRY mode + scan-on-demand (first deregistration attempt wedged in periph destroy_dev teardown under the sim lock), marker-ccb callback instead of heap-spray reclaim (spray missed the chunk twice on this allocator), direct write of the freed chunk for the crash demo, mode-4 fix-validation trail
Verified recommended fix
Pin the sim (atomic ref under cam_simq_spin) while linked on cam_simq; camisr drops the pin after processing; rewrite cam_sim_release() as an atomic cmpset protocol (xpt_sim_finalize under cam_simq_spin for the 1->0 transition); pin cam_dead_sim.refcount high -- see validated fix.diff
Verdict
cam_sim_release() (cam_sim.c:241-256) kfrees the cam_sim with zero synchronization against the CAM completion softint: xpt_done() links the sim onto the global cam_simq lock-free from interrupt context (cam_xpt.c:4901-4917), camisr pops it and only then blocks on the sim lock (cam_xpt.c:7395-7406), and the standard driver detach (lock; xpt_bus_deregister(); cam_sim_free(); unlock -- ahci_cam.c:210-227 et al.) completes the whole free while camisr is blocked, so the softint subsequently writes the freed chunk (flags RMW at 7403, lockmgr at 7402, spin_lock/doneq walk at 7415-17) and dispatches (*ccb_h->cbfcnp)() from freed-object contents (7506). Deterministically reproduced on the stock guest kernel with a production-symbol mini-SIM rig: mode 1 executed a completion callback out of a kfree'd sim (marker_hit=1, silent); mode 2 (poisoned freed chunk) stopped the guest in DDB at camisr_runqueue+0x53 (page fault on the wild doneq head). Trigger class is controller detach racing completion (root kldunload / virtio destroy / surprise removal + softint delay), so recorded impact is the proven panic/DoS; the function-pointer dispatch from freed memory is the documented uid0 ceiling on this unhardened guest (no SMAP/SMEP/KASLR), not weaponized in this run.
No comments yet.