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

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-757 takes sc->lock only around TAILQ_REMOVE/nsess--, releases it at 757,
  • then at isc_sm.c:772 executes sc->sessions[sp->sid] = NULL; and
  • at isc_sm.c:777 kfree(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_t slab 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).

  1. As root: kldload iscsi_initiator; run iscontrol (or the Python stub) to log in to a target exposing β‰₯1 LUN so CAM attaches /dev/da0 and ic_fullfeature sets ISC_FFPHASE.
  2. 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.

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

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2010 Β· 5 files
FileTypeDescriptionSize
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
VERDICT.md verdict Source verification narrative
↓ download 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_testable
baseline reproduced→ patch + rebuild →patched clean

not_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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Confirmed kernel references

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.