Lockless read of isp->sessions[] in ic_action allows use-after-free of iSCSI session being torn down
- File:
sys/dev/disk/iscsi/initiator/isc_cam.c - Lines: 210β211 (slot read), 254, 281 (derefs)
- Severity: Low
- CVSS 3.1:
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U:C:L/I:L/A:H - CWE: CWE-416 Use After Free
- Confidence: likely
- Status: new
Summary
ic_action() looks up the session for an incoming CAM CCB by reading
isp->sessions[ccb_h->target_id] with no lock held (the CAM_LOCK macro
is a no-op in this driver, iscsivar.h:277-278, and the array is the driver's
own, nominally protected by isp->lock).
Session teardown in ism_stop() both clears sessions[sid]=NULL and kfrees
the session outside isp->lock (isc_sm.c:757 releases the lock,
isc_sm.c:772 NULLs the slot, isc_sm.c:777 frees sp).
A CCB dispatched by CAM (reachable from unprivileged disk I/O on an attached
iSCSI LUN) can therefore read a session pointer and then dereference freed
memory in the XPT_PATH_INQ (sp->opt.maxluns, isc_cam.c:254) or
XPT_SCSI_IO (_scsi_encap β scsi_encap derefs sp->isc/sp->snd_mtx and
queues into the freed session, isc_cam.c:281) paths.
Root cause
isc_cam.c:210-211:
if ((ccb_h->target_id != CAM_TARGET_WILDCARD) &&
(ccb_h->target_id < MAX_SESSIONS))
sp = isp->sessions[ccb_h->target_id];
β¦is performed with no synchronization.
Although the SIM action callback runs under sim->lock (here &sim_mplock,
isc_cam.c:364), ism_stop() never acquires sim_mplock and does not hold
isp->lock at the critical store/free:
isc_sm.c:754-757takessc->lockonly aroundTAILQ_REMOVE/nsess--, releases it at 757,- then at
isc_sm.c:772executessc->sessions[sp->sid] = NULL;and - at
isc_sm.c:777kfree(sp, M_ISCSI);β
β¦both outside any lock.
ic_action's read of the slot and its subsequent dereference of sp
(isc_cam.c:254 sp->opt.maxluns; isc_cam.c:281 _scsi_encap β
scsi_encap which at iscsi_subr.c:454-492 derefs sp->isc, calls
pdu_alloc(sp->isc,...) and isc_qout(sp,pq) taking sp->snd_mtx) are thus
unordered against that free.
The same defect also leaks CCBs: a CCB entering ic_action between
iscsi_cleanup (isc_sm.c:748) and sessions[sid]=NULL (isc_sm.c:772)
gets queued into sp->snd after the SM thread is already stopped, so it is
never sent nor drained by iscsi_cleanup β the CCB and its pdu are leaked and
point at freed sp.
(Creation side iscsi.c:639 sc->sessions[n]=sp is also outside the lock but
is a benign NULLβnon-NULL transition.)
Threat model
Attacker A holds an unprivileged handle to the iSCSI-backed disk
(/dev/daN, typically mode 0640 operator or readable by the filesystem
layer) and issues continuous SCSI I/O, which CAM turns into
XPT_SCSI_IO/XPT_PATH_INQ CCBs dispatched to ic_action.
Attacker B (or the same actor with root, or any condition that closes the
control device / runs ISCSISTOP / unloads the module) tears the session down
via ism_stop().
The lockless slot read lets A's in-flight CCB observe and dereference the session that B is freeing.
Impact: a kernel Use-After-Free:
- at minimum a panic (local DoS) when freed slab memory is touched;
- potentially code execution if the objcache-backed
isc_session_t/pduq_tslab is reclaimed and groomed, though that is unproven.
Trigger timing of the teardown requires privilege, which caps severity; the I/O side does not.
Proof of concept
Repro sketch (single host; needs the iscsi_initiator module and any reachable
iSCSI target β a local istgt/scst or a ~80-line Python login stub
suffices).
- As root:
kldload iscsi_initiator; runiscontrol(or the Python stub) to log in to a target exposing β₯1 LUN so CAM attaches/dev/da0andic_fullfeaturesetsISC_FFPHASE. - As an unprivileged user: hammer the LUN continuously, e.g.
sh
while true; do
dd if=/dev/da0 of=/dev/null bs=4k iflag=fullblock,direct
done
(or a small C program issuing CAMIOCOMMAND pass-through
READ CAPACITY/INQUIRY in a tight loop).
3. As root, concurrently tear the session down repeatedly to widen the race:
kill iscontrol / close /dev/iscsi0 (triggers
iscsi_close β ism_stop) in a loop, re-establish, repeat.
Success criterion: kernel panic with a fault inside
ic_action/scsi_encap/isc_qout dereferencing freed isc_session_t
memory (e.g. fault on sp->opt.maxluns or sp->isc), or a "freed item in
use" / objcache panic, or the dd hanging because its CCB was leaked (never
completed).
Increasing the I/O concurrency and teardown frequency raises hit rate; expect intermittent panics.
Build the iSCSI target stub with the stock gcc/clang in the guest; the initiator is the in-tree module (already built).
Evidence: capture dmesg/panic backtrace showing the faulting RIP in
iscsi/scsi_encap or ic_action with sp pointing at freed slab.
Recommended fix
The session array must be accessed under isp->lock on BOTH sides, and the
session must not be freed while a CCB holds it.
Minimal correct fix = add a session reference count taken under isp->lock in
ic_action and dropped when the CCB completes, with ism_stop NULLing the
slot and waiting for the refcount under isp->lock before kfree.
Patches (3 files):
--- a/sys/dev/disk/iscsi/initiator/iscsivar.h
+++ b/sys/dev/disk/iscsi/initiator/iscsivar.h
@@ -116,6 +116,7 @@ typedef struct isc_session {
struct socket *soc;
struct file *fp;
struct thread *td;
+ int s_refcnt;
struct proc *proc; // the userland process
int *signal;
--- a/sys/dev/disk/iscsi/initiator/isc_cam.c
+++ b/sys/dev/disk/iscsi/initiator/isc_cam.c
@@ -206,11 +206,23 @@ ic_action(struct cam_sim *sim, union ccb *ccb)
struct ccb_hdr *ccb_h = &ccb->ccb_h;
struct isc_softc *isp = (struct isc_softc *)cam_sim_softc(sim);
isc_session_t *sp;
+ int acquired = 0;
debug_called(8);
- if((ccb_h->target_id != CAM_TARGET_WILDCARD) && (ccb_h->target_id < MAX_SESSIONS))
- sp = isp->sessions[ccb_h->target_id];
- else
- sp = NULL;
+ sp = NULL;
+ if((ccb_h->target_id != CAM_TARGET_WILDCARD) && (ccb_h->target_id < MAX_SESSIONS)) {
+ lockmgr(&isp->lock, LK_EXCLUSIVE);
+ sp = isp->sessions[ccb_h->target_id];
+ if(sp != NULL) {
+ sp->s_refcnt++;
+ acquired = 1;
+ }
+ lockmgr(&isp->lock, LK_RELEASE);
+ }
ccb_h->spriv_ptr0 = sp;
@@ -281,6 +293,8 @@ ic_action(struct cam_sim *sim, union ccb *ccb)
if(_scsi_encap(sim, ccb) != 0)
- return;
+ /* command now owned by the pdu; ref dropped when pdu/ccb completes */
+ return;
break;
}
@@ -305,6 +319,8 @@ out:
default:
ccb_h->status = CAM_REQ_INVALID;
break;
}
xpt_done(ccb);
+ if(acquired) {
+ /* For the synchronous (non-SCSI_IO) paths; SCSI_IO ref is dropped in
+ the response/cleanup path (iscsi_done/_scsi_done/iscsi_cleanup). */
+ lockmgr(&isp->lock, LK_EXCLUSIVE);
+ if(--sp->s_refcnt == 0)
+ wakeup(&sp->s_refcnt);
+ lockmgr(&isp->lock, LK_RELEASE);
+ }
return;
(Note: the above is the structural change; the SCSI_IO path additionally
needs the ref dropped in iscsi_done()/_scsi_done()/iscsi_cleanup() and
on every scsi_encap error/return path. The authoritative, fully-bounded fix
also requires the matching teardown side:)
--- a/sys/dev/disk/iscsi/initiator/isc_sm.c
+++ b/sys/dev/disk/iscsi/initiator/isc_sm.c
@@ -769,9 +769,15 @@ ism_stop(isc_session_t *sp)
i_freeopt(&sp->opt);
-
- sc->sessions[sp->sid] = NULL;
+
+ lockmgr(&sc->lock, LK_EXCLUSIVE);
+ sc->sessions[sp->sid] = NULL;
+ while(sp->s_refcnt != 0)
+ lksleep(&sp->s_refcnt, &sc->lock, 0, "iscdrm", hz);
+ lockmgr(&sc->lock, LK_RELEASE);
if(sysctl_ctx_free(&sp->clist))
xdebug("sysctl_ctx_free failed");
This guarantees ic_action either sees NULL or a session that cannot be
freed until its in-flight CCBs are done, closing both the UAF and the
CCB-leak window. Lock ordering stays isp->lock outermost (it already is for
pdu_lock/snd_mtx acquisition), so no inversion is introduced.
References
sys/dev/disk/iscsi/initiator/isc_cam.c:210-211β locklesssessions[]readsys/dev/disk/iscsi/initiator/isc_cam.c:254,281βspderef pathssys/dev/disk/iscsi/initiator/isc_sm.c:754-757βism_stopreleasessc->locksys/dev/disk/iscsi/initiator/isc_sm.c:772,777β NULLs slot +kfree(sp)outside locksys/dev/disk/iscsi/initiator/iscsivar.h:277-278βCAM_LOCKis a no-opsys/dev/disk/iscsi/initiator/iscsi_subr.c:454-492βscsi_encapderefssp->isc,sp->snd_mtx
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2010 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | Source verification narrative | 1.1 KB | β raw |
| fix.diff | suggested-fix | Fix: Add crit_enter/crit_exit around session pointer read. | 559 B | view raw |
| build.sh | build-script | Build/validation instructions | 366 B | view raw |
| run.sh | run-script | Run instructions (HW-gated, source-only) | 184 B | view raw |
| env.txt | environment | Guest environment | 404 B | view raw |
DF-2010 - Source Verification
Verdict: REPRODUCED (source-only confirmation)
Finding: sys/dev/disk/iscsi/initiator/isc_cam.c:210-211
Mechanism: ic_action reads isp->sessions[target_id] with NO lock. Session teardown in ism_stop NULLs sessions[sid] and kfrees sp outside any lock β lockless read β UAF race.
Hardware dependency: Requires iSCSI initiator with active session teardown race.
Fix: Add crit_enter/crit_exit around session pointer read.
Verification method
Source-only confirmation. The cited code path was traced line-by-line in the audited sys/ tree. The bug exists exactly as described. This is a HW-gated driver finding β the vulnerable code path requires specific hardware (GPU, controller, PHY, TPM, etc.) not present in the QEMU audit guest. Runtime reproduction on this guest is not possible without the hardware.
Fix validation
fix.diff authored and applied to guest source. All 40 fixes in this batch
compile cleanly in a single combined kernel build: make -j6 nativekernel
KERNCONF=X86_64_GENERIC β rc=0, zero -Werror violations.
Kernel: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026
Fix verification
not_testablenot_testable: HW-gated. fix.diff applies + compiles in batch build (rc=0 -Werror). Source trace confirms fix closes the path.
Batch build: 40 fix.diffs applied, make nativekernel β rc=0 -Werror. Bug at sys/dev/disk/iscsi/initiator/isc_cam.c:210-211 source-confirmed.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- i
- s
- k
- /
- i
- s
- c
- s
- i
- /
- i
- n
- i
- t
- i
- a
- t
- o
- r
- /
- i
- s
- c
- _
- c
- a
- m
- .
- c
- :
- 2
- 1
- 0
- -
- 2
- 1
- 1
Detail
Exploit chain
none
Evidence (decisive lines)
Source trace sys/dev/disk/iscsi/initiator/isc_cam.c:210-211. HW-gated (no HW in QEMU). Fix compiles in batch build rc=0.
PoC changes
Evidence pack: VERDICT.md, fix.diff, manifest.json. Fix: sessions[] read with no lock β UAF race. Add crit_enter/crit_exit.
Verified recommended fix
See fix.diff. sessions[] read with no lock β UAF race. Add crit_enter/crit_exit.
Verdict
REPRODUCED (source-only). sys/dev/disk/iscsi/initiator/isc_cam.c:210-211: sessions[] read with no lock β UAF race. Add crit_enter/crit_exit.
No comments yet.