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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5592 Β· 14 files
FileTypeDescriptionSize
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 on sim->sim_doneq under sim->sim_spin only, then links the sim onto the global cam_simq under cam_simq_spin and calls setsoftcambio(). This is the contract controller interrupt handlers rely on.
  • swi_cambio β†’ camisr() (cam_xpt.c:7389-7407) pops the sim off cam_simq under cam_simq_spin and only then takes CAM_SIM_LOCK(sim) β€” so a detach thread that already holds the sim lock runs its entire xpt_bus_deregister() + cam_sim_free() while camisr is blocked holding nothing but a raw struct 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).

VERDICT.md
↓ download raw

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:

  1. Silent UAF with control-flow proof (mode 1). A marker ccb was queued on the dying sim's sim_doneq immediately before cam_sim_free() (exactly what a late interrupt-context xpt_done() does at cam_xpt.c:4904-4907). After the free + lock release, camisr_runqueue popped it and executed (*ccb_h->cbfcnp)(...) (cam_xpt.c:7506) β€” a kernel function-pointer call dispatched out of a kfree()'d object. marker_hit=1; the guest kept running (silent corruption).

  2. Crash proof (mode 2). The freed chunk's sim_doneq.tqh_first was set to 0x00dead000 (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 of flags).
  • lockmgr() on the freed embedded struct lock (7402) β€” lock-count/owner writes into freed memory.
  • spin_lock(&sim->sim_spin) + TAILQ walk of the freed sim_doneq (7415-17).
  • Function-pointer dispatch (*ccb_h->cbfcnp)(path->periph, ccb) (7506): whoever reallocates the 168-byte M_CAMSIM chunk controls cbfcnp, 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_int under cam_simq_spin) when linking it on cam_simq; (b) camisr() clears CAM_SIM_ON_DONEQ for popped sims under the same spin and drops the pin with cam_sim_release(sim, 0) only after processing+unlock; (c) cam_sim_release() rewritten as an atomic cmpset loop; the final 1β†’0 transition happens in xpt_sim_finalize() under cam_simq_spin so it cannot interleave with the pin acquisition; (d) cam_dead_sim.refcount pinned high (0x40000000) so the static dead sim can never be finalized.
  • Built as make nativekernel KERNCONF=X86_64_GENERIC on 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) and marker_hit=1 on freed memory.
  • fixed: chunk refcount now reads 1 (pin held β‡’ sim NOT freed), marker_hit=1 with 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

  1. First build: rig panicked in its own boot scan (LK_CANRECURSE missing) β€” rig bug, fixed.
  2. 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.)
  3. 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.
  4. Mode 2: spray missed again; replaced with direct write of the freed chunk (post-UAF standard step) β†’ camisr_runqueue fault, DDB stop.
  5. Fix build: buildkernel refused (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 by cam_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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-5592

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.