ism_stop frees session while CAM/ic_action and sysctl handlers can still dereference it (UAF)
Summary
ism_stop() tears session down in-place: calls ic_lost_target() drops from sc->isc_sess destroy_dev()s cdev uninits all mutexes i_freeopt()s options then sets sc->sessions[sp->sid]=NULL and finally kfree()s sp. CAM ic_action() reads sc->sessions[target_id] and dereferences sp with NO lock and per-session sysctl handlers hold raw sp pointer from arg1 no lifetime guarantee. Window between ic_lost_target() reporting device lost and sc->sessions[sid]=NULL during which CAM ccb or sysctl read can obtain about-to-be-freed sp and dereference it after kfree(). sysctl_ctx_free removes OIDs but does not wait for handlers already executing. [VERIFIED pass2: reproduced deterministically - parked ic_action reads attacker value 0x51515150 from freed reclaimed chunk, parked sysctl handler exports freed-chunk contents via SYSCTL_OUT as uid 1001 through world-readable OIDs, scsi_encap write path WRITES into the freed chunk and garbage reclaim variant Fatal trap 12 supervisor write at scsi_encap+0x445; xpt_freeze drain is ifdef 0d out; refcount-plus-drain-plus-sid-keyed-sysctl fix-validated - S5 guest survives, teardown drains while victims read live values; privesc closed - sysctl path read-only ints, write primitive needs privileged CAM access]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2462 Β· 31 filesDF-2462 PoC β ism_stop frees the session while CAM/ic_action and the per-session sysctl handlers can still dereference it (UAF)
- File under test:
sys/dev/disk/iscsi/initiator/isc_sm.c:724-777(ism_stop), with the victim pathsisc_cam.c:201-309(ic_action) andisc_sm.c:630-676(isc_dump_stats,isc_sysctl_targetName/Address). - Root cause:
ism_stop()tears a session down IN PLACE. Between the CAM path teardown (ic_lost_target, isc_sm.c:752) andkfree(sp)(:777) the session stays reachable:sc->sessions[sp->sid]is cleared only at :772 and the per-session sysctl OIDs only at :774 (sysctl_ctx_free) β and neitheric_action()nor the sysctl handlers hold a lock or reference across their lookupβuse, so any reader that already holdsspkeeps dereferencing freed memory. Thexpt_freeze_devqdrain that would stop in-flight CAM traffic is#if 0'd out inic_freeze(isc_cam.c:57-63). - Windows (line-accurate):
- W-CAM β from entry into
ism_stop(the :740-744 xmitter wait loop can sleep up tohzper iteration;iscsi_cleanupdoes logout I/O) untilsessions[sid]=NULL(:772):ic_action()'s unlockedsp = isp->sessions[target_id](isc_cam.c:211) returns the dying session. After :772 any reader that already loadedspstill holds it (no refcount) β unbounded. - W-SYSCTL β the OIDs
...iscsiN.{targetname,targeaddress,stats,douio}stay registered until :774, and their handlers hold rawsp(stats: arg1 =sp) or raw&sp->opt.targetNamepointers; a handler entered any time before removal dereferencesspwith no lifetime guarantee.i_freeopt(:771) frees the option strings BEFOREsysctl_ctx_free(:774) removes the OIDs that resolve through them. - W-AGGRAVATOR β if
sysctl_ctx_freefails under concurrent readers it returns an error thatism_stopmerely logs (:774-775); the session is still kfree'd at :777 with the OIDs left REGISTERED pointing into the freed chunk (observed live on the guest: "can't re-use a leaf (stats)!" on the next session's registration while an unprivileged user hammered the OID). - Trigger preconditions (real driver): a configured iSCSI session being
torn down (iscontrol exits / is killed β
iscsi_closeβism_stop, iscsi.c:176; module unload βiscsi_stopβism_stop, iscsi.c:851) while (a) CAM commands to the iscsi bus are in flight, or (b) ANY user reads the per-session sysctl OIDs (world-readable, no privilege needed β proven as uid 1001 in scenario S6).
Rig
Kernel-compiled verbatim interposition: df_pre.c + <verbatim isc_sm.c>
+ <verbatim isc_cam.c: ic_lost_target/_inq/_scsi_encap/ic_action>
+ <verbatim iscsi_subr.c: dwl/scsi_encap> + <verbatim isc_subr.c:
i_freeopt> + df_post.c compiled into X86_64_GENERIC as
dev/disk/iscsi/initiator/dfiscsi_harness.c (sha256-gated against the
audit tree by build.sh). All interposition is #define-renames of
external functions (xpt_async/xpt_free_path/xpt_done shims,
isc_stop_receiver/iscsi_cleanup/i_pdu_flush/release_dev/destroy_dev
shims, kfree β free+reclaim-and-poison, ksnprintf/kprintf β
stall hooks); the driver text is byte-for-byte the gated source.
Everything else is real: lockmgr, TAILQ ops, mtx_init/mtx_uninit,
sysctl_ctx_free, kmalloc/kfree(M_ISCSI), i_freeopt, pdu_alloc, and the
real userland sysctl path for the per-session OIDs.
Stall points (deterministic scheduling of the race):
* teardown parked inside ic_lost_target (the xpt_async shim at
isc_cam.c:90) β in-window;
* CAM victim parked in ic_action between the sessions[] lookup
(:211/:215) and the dereferences (:254 / _scsi_encap) β via the
debug(4,"func_code...") kprintf at :217;
* sysctl victim parked inside isc_dump_stats before touching sp β via
the first ksnprintf (isc_sm.c:642).
Poison/reclaim: on the session kfree, the harness immediately
reclaims the chunk (same size/type β the zone returns the same address)
and fills it as an attacker-controlled object (FIXUP: valid
mutexes/queues + chosen field values; FAULT: valid mutexes + garbage
queue heads).
Build / run
./build.sh # gate, push, generate TU, nativekernel, install, reboot ./run.sh # baseline battery S0..S6, S7, then S5 (kills guest) ./make_fixed.sh # apply fix.diff on the guest, rebuild, reboot ./run_fixed.sh # A/B battery on the fixed kernel (S5 must survive)
Scenario map:
* S0 control (live session; values are the A/B reference)
* S1 in-window concurrent CAM read (teardown parked after ic_lost_target)
* S2 UAF READ via verbatim ic_action (PATH_INQ reads sp->opt.maxluns
from the freed chunk β cpi.max_lun)
* S3 UAF READ/LEAK via verbatim isc_dump_stats (freed-chunk contents
exported through SYSCTL_OUT)
* S4 UAF WRITE via verbatim ic_actionβscsi_encapβdwl/isc_qout/
i_nqueue_csnd (writes INTO the freed, attacker-reclaimed chunk)
* S5 deterministic fault: same as S4 but the reclaim leaves garbage
queue heads / lun table β kernel page fault (guest dies; baseline
Fatal trap 12 ... scsi_encap+0x445, the dwl indexed write
sp->target_lun[sp->target_nluns++] = lun with an
attacker-controlled 32-bit index)
* S6 unprivileged userland reads of the session OIDs while the
teardown is parked in-window (uid 1001)
* S7 natural race: unpriv userland sysctl hammer against a
create/teardown loop with garbage-isc reclaim
Expected on the verbatim kernel
- S0:
PATH_INQ: status=0x1 max_lun=2, statsrecv=111 sent=222 ..., clean teardown. - S1: S6: reads succeed against the being-torn-down session
(
sessions[7]still set; live values returned to uid 1001). - S2:
*** UAF READ PROVEN ***βcpi.max_lun=0x51515150(0x51515151 planted in the reclaimed chunk). - S3:
*** UAF READ/LEAK PROVEN ***β planted values (flags=0x50505050,cws/cmd/exp/max/stat/itt = 0x65..-0x6a..) surface in the handler output. - S4:
*** UAF WRITE PROVEN ***β ncsnd incremented, csnd.tqh_first linked, target_lun[0]/target_nluns written inside the freed chunk. - S5: guest dies:
Fatal trap 12 ... supervisor write data ... Stopped at scsi_encap+0x445: movl %r13d,0x9c(%rbx,%rdx,4). - S7: no natural hit in the runs performed (probabilistic); the ctx_free-failure aggravator (stale registered OIDs pointing at the freed session) was observed live.
Expected on the fixed kernel (A/B)
- S0 bit-identical to baseline (same status, same stats string).
- S1:
PATH_INQ: status=0x8 (CAM_DEV_NOT_THERE)during the window. - S2/S3/S4:
teardown state after 3s: stop_done=0 free_seen=0β ism_stop is HELD in the reference drain while the victim uses the session; victims observe LIVE values only; no freed chunk exists to read or corrupt while they run. - S5:
GUEST SURVIVED(baseline: Fatal trap 12). - S6: the unprivileged reads surface no session data (handler returns ENODEV via the sid lookup; the sysctl tool prints no value).
Fix
fix.diff (4 files): detach sc->sessions[sid]=NULL to the top of
ism_stop under sc->lock; add sp->refs + isc_sess_lookup()/isc_sess_rele();
ic_action looks up under sc->lock and takes a reference (released
before returning); the per-session sysctl handlers take the sid as
arg1 and resolve the session through isc_sess_lookup() (returning
ENODEV once detached); ism_stop drains sp->refs before destroying
the session and runs sysctl_ctx_free before i_freeopt.
DF-2462 VERDICT β verified
Finding. ism_stop() (sys/dev/disk/iscsi/initiator/isc_sm.c:724-777)
destroys a session in place while two unlocked reader populations can
still hold sp: CAM's ic_action() (sp = isp->sessions[target_id],
isc_cam.c:211, no lock, no refcount; dereferenced at :215/:254 and
through _scsi_encapβscsi_encapβdwl/isc_qout/i_nqueue_*)
and the per-session sysctl handlers (isc_dump_stats etc., raw sp
as arg1; OIDs registered until :774). The session stays in
sc->sessions[] until :772 and is kfree'd at :777.
Status: REPRODUCED on the QEMU guest with a kernel-compiled
verbatim-interposition harness (whole isc_sm.c + the
ic_lost_target/_inq/_scsi_encap/ic_action/dwl/scsi_encap/
i_freeopt functions compiled byte-for-byte from the sha256-gated
audit tree; all interposition via #define renames of externals).
Every primitive was demonstrated by driving the REAL code:
- S2 β UAF read (CAM), certain. A victim thread parked inside
verbatim
ic_actionbetween the sessions[] lookup and thesp->opt.maxlunsread (thedebug(4,"func_codeβ¦")call at isc_cam.c:217 compiles, through iscsivar.h:40-41, to a kprintf the harness intercepts). The teardown completed for real (ism_stoptokfree); the harness then reclaimed the freed chunk (same address β the M_ISCSI zone is LIFO) and plantedopt.maxluns = 0x51515151. Resuming the victim:cpi.max_lun = 0x51515150β the value read out of the FREED chunk was attacker-controlled. (run.log: "UAF READ PROVEN".) - S3 β UAF read/info-leak (sysctl), certain. A victim parked
inside verbatim
isc_dump_statsbefore touchingsp(firstksnprintf, isc_sm.c:642); teardown completed (sysctl_ctx_freereally deregistered the OIDs,kfreereally freed, chunk reclaimed with planted values); the handler then formatted and exportedflags=0x50505050 pdus-alloc=0 pdus-max=0 cws=1701143909 cmd=66666666 exp=67676767 max=68686868 stat=69696969 itt=6a6a6a6aβ seven fields read from the freed chunk, surfaced through SYSCTL_OUT. This handler is exactly what an unprivileged user invokes withsysctl iscsi.iscsiN.stats. - S4 β UAF write (CAM), certain. Victim parked in
ic_actionwith a SCSI_IO CCB; teardown freed + reclaimed the chunk (FIXUP: attacker object with valid mutexes/queues); the resumed verbatim path WROTE into the freed chunk, all observed:stats.ncsnd0x52525252β0x52525253 (i_nqueue_csnd),csnd.tqh_firstlinked to a live pdu (TAILQ_INSERT_TAIL),target_lun[0]0xbebebebeβ0 andtarget_nluns0β1 (dwl), plusflags |= ISC_OQNOTEMPTY(isc_qout). - S5 β deterministic kernel fault, certain. Same as S4 but the
reclaim models an unrelated allocation's garbage: the victim's
dwlexecutessp->target_lun[sp->target_nluns++] = lunwith the garbage 32-bit index β write atsp + 0x9c + 4*idx:Fatal trap 12, supervisor write data, page not present β¦ Stopped at scsi_encap+0x445: movl %r13d,0x9c(%rbx,%rdx,4)β guest down in DDB (panic.txt, panic.s5.fresh.txt). This is a relative-write primitive with an attacker-influenced index and value (idx = reclaimedtarget_nluns, value = CCB lun). - S1/S6 β window reality + unprivileged reachability, certain.
With the teardown parked just after
ic_lost_target(the xpt_async shim = isc_cam.c:90),sc->sessions[7]is still set and the OIDs are still registered; uid 1001 (maxx) read all four session OIDs from userland and got live values. The unprivileged user can be the racing reader; only the teardown side needs the root-configured session (iscontrol dying, or the remote target/network dropping the connection β iscsi.c:176 / iscsi_subr.c:344-372 paths). - S7 β natural race, not hit in the runs performed (7531 and
14271 unprivileged hammer reads against ~1-2.6k teardown cycles;
the exploitable preemption window inside the 10-line handler is
~Β΅s). The deterministic proofs above carry the finding. S7 did
however surface the aggravator live: under concurrent readers
sysctl_ctx_freeFAILS,ism_stoponly logs it (:774-775), and the session is kfree'd with the OIDs still registered β the next registration printed "can't re-use a leaf (stats)!" β i.e. the tree keeps OIDs whose arg1 points into freed memory until reboot.
Fix validation (A/B, certain). fix.diff (early
sessions[sid]=NULL under sc->lock at the top of ism_stop;
sp->refs + isc_sess_lookup/rele; ic_action lookup under the
lock + reference; sid-based sysctl handlers returning ENODEV;
drain before destruction; sysctl_ctx_free before i_freeopt):
same harness around the PATCHED verbatim text, same battery:
* S0 bit-identical to baseline (status/max_lun/stats string) β
normal path unchanged;
* S1: status=0x8 (CAM_DEV_NOT_THERE) during the window;
* S2/S3/S4: teardown state after 3s: stop_done=0 free_seen=0 β
ism_stop is HELD in the drain while the victim runs; victims
observe only LIVE values (S2: max_lun=0x2; S3: the exact S0 string;
S4: "no freed chunk exists to corrupt");
* S5: GUEST SURVIVED (baseline: Fatal trap 12);
* S6: unprivileged reads surface no session data (handler ENODEV).
Privilege-escalation assessment (closed with facts).
No unprivβroot chain is demonstrated, and none is realistic here:
1. The unprivileged reader (sysctl) reaches isc_dump_stats/
targetname handlers β a READ-only path. Demonstrated ceiling for
that user: (a) leak of freed-chunk contents (S3 β with attacker
reclaim this is controlled-data echo, without it stale session
fields), and (b) a kernel fault when the freed chunk is reused by
unrelated data (S5-class dereference of garbage sp->isc /
queue heads) β local DoS from an unprivileged user racing a
root-initiated teardown. Turning (a) into a useful KASLR/heap
leak yields fixed-offset ints, not pointers; the strings handlers
read *cp which i_freeopt NULLs, so they do not leak adjacent
heap either.
2. The WRITE primitive (S4/S5: TAILQ links + indexed
target_lun[] write with attacker-controlled index/value) rides
the CAM path (ic_action β scsi_encap), which in the real
driver is reachable only through the iscsi SIM's registered bus β
issuing CCBs to it requires the da periph, i.e. root/operator
access to the iscsi-backed disk, or in-flight I/O at teardown
time (root-configured workload). An already-privileged actor
corrupting freed memory has no escalation to gain.
3. The reclaim-controlling attacker in S4 writes into memory they
already own (their own reallocated chunk); the dangerous
direction β the kernel later walking attacker-forged queue heads
in sp->hld/csnd β requires the session's sm thread and receiver
to still run, but ism_stop stops both (:734-744) before the
free. The post-free readers demonstrated here (ic_action,
sysctl) only read fields and push one pdu onto a queue nobody
drains.
Conclusion: real, proven memory-safety violation whose realistic
impact is unprivileged info-leak/DoS during legitimate teardown plus
privileged-context corruption β consistent with (and arguably at the
top of) the filed Medium severity; NOT a practical uid-0 vehicle.
(The upstream FreeBSD initiator was rewritten wholesale (iscsi/icl)
years ago; this DF driver is on the legacy list.)
Harness bugs found and fixed during verification (documented so
the rig is reusable): (1) blocking inside a sysctl handler deadlocks
against sysctl_ctx_free's SYSCTL_XLOCK because
kern_sysctl.c:1573-1576 holds the pcpu SHARED sysctllock across the
handler β the control interface now returns immediately and run.sh
polls; (2) a single sleep channel for all gate flags livelocked
concurrent waiters β per-flag channels now; (3) S2/S3/S4 originally
ran the teardown inline, which the FIXED kernel's drain (correctly)
blocks β teardown now runs in its own thread with a settle gap.
None of these touched the driver text or the demonstrated outcomes.
Artifacts. run.log (baseline battery, kernel #4), run.s7.log, run.s5.log, panic.txt / panic.s5.fresh.txt (identical S5 fault signatures from two independent baseline batteries), leak_sample.txt (S3/S2 across runs), run_fixed.log, run_fixed_s5.log ("GUEST SURVIVED"), run_fixed_s7.log, s6_fixed_read.log, build.log / fix_build.log / fix_installkernel.log / fixed_gate.log, gen.log / gen_fixed.log, serial_fixed_debug.txt, env.txt, df_pre.c/df_post.c/gen.sh, build.sh/run.sh/make_fixed.sh/ run_fixed.sh, fix.diff.
Bottom line: the UAF is real, reachable, and mechanically proven on both victim populations (CAM read+write, sysctl read/leak), it kills the guest when the freed chunk is reused, the proposed fix.diff closes every demonstrated window while keeping the normal path bit-identical, and the escalation ceiling is leak/DoS β no unprivβroot chain exists on this path.
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
fixedConfirmed kernel references
- sys/dev/disk/iscsi/initiator/isc_sm.c:724-777
- sys/dev/disk/iscsi/initiator/isc_sm.c:752
- sys/dev/disk/iscsi/initiator/isc_sm.c:771
- sys/dev/disk/iscsi/initiator/isc_sm.c:772
- sys/dev/disk/iscsi/initiator/isc_sm.c:774
- sys/dev/disk/iscsi/initiator/isc_sm.c:777
- sys/dev/disk/iscsi/initiator/isc_sm.c:630-653
- sys/dev/disk/iscsi/initiator/isc_cam.c:210-215
- sys/dev/disk/iscsi/initiator/isc_cam.c:254
- sys/dev/disk/iscsi/initiator/isc_cam.c:281
- sys/dev/disk/iscsi/initiator/iscsi_subr.c:441-528
- sys/dev/disk/iscsi/initiator/iscsi.c:176
- sys/dev/disk/iscsi/initiator/iscsi.c:851
Detail
Exploit chain
Unprivileged user (any uid) polls sysctl iscsi.iscsiN.stats / targetname while the root-configured session is torn down (iscontrol exit, kill, connection loss). If the handler is entered before sysctl_ctx_free (isc_sm.c:774) and runs past kfree (:777), it reads the freed chunk: stale-session info leak, or controlled-data echo if the chunk was reclaimed; if the reclaim holds unrelated data (iscs garbage, queue heads) the read faults - unprivileged kernel panic (deterministic analogue proven as S5 via the CAM victim: Fatal trap 12 write at scsi_encap+0x445 from dwl sp->target_lun[sp->target_nluns++]=lun with attacker-influenced index). The write primitive (S4: ncsnd/csnd links/target_lun table inside the freed chunk) requires issuing CAM CCBs to the iscsi bus, i.e. privileged disk access in the real driver. No uid-0 chain.
Evidence (decisive lines)
run.log lines DF2462[S2]/[S3]/[S4] PROVEN; panic.s5.fresh.txt (Fatal trap 12, Stopped at scsi_encap+0x445, movl %r13d,0x9c(%rbx,%rdx,4)); leak_sample.txt (planted values in two independent baseline runs); run_fixed.log (teardown state stop_done=0 free_seen=0, live values); run_fixed_s5.log (GUEST SURVIVED); s6_fixed_read.log (uid 1001 reads empty on fixed vs live values on baseline run.log); fixed_gate.log (patched-tree sha256s)
PoC changes
Evidence pack authored from scratch (no seed). Harness iterations: (1) fixed a self-deadlock - blocking inside the control sysctl handler pins the pcpu SHARED sysctllock (kern_sysctl.c:1573) against sysctl_ctx_free SYSCTL_XLOCK; control now returns immediately with a busy flag; (2) fixed a gate livelock - all waiters slept on one channel and wakeup-ping-ponged, burning the iteration budget in ms; per-flag channels; (3) moved S2/S3/S4 teardown into its own thread so the FIXED kernel drain (correctly) blocking it does not stall the victim release; (4) -Werror fixes (redundant-decls ordering, unused shims). None touched the verbatim driver text or the demonstrated outcomes.
Verified recommended fix
ism_stop: NULL sc->sessions[sid] under sc->lock FIRST; add sp->refs drained before destruction; ic_action + per-session sysctl handlers look the session up under sc->lock with a reference (handlers keyed by sid, ENODEV after detach); run sysctl_ctx_free before i_freeopt.
Verdict
Reproduced with kernel-compiled verbatim interposition on the QEMU guest (whole isc_sm.c plus the ic_action/_inq/_scsi_encap/ic_lost_target/dwl/scsi_encap/i_freeopt functions compiled byte-for-byte from the sha256-gated tree; interposition only via define renames). ism_stop tears the session down in place while ic_action reads sessions[target_id] with no lock or refcount (isc_cam.c:211) and the per-session sysctl handlers hold raw sp as arg1 until sysctl_ctx_free at isc_sm.c:774. Demonstrated: UAF READ via ic_action with attacker-planted 0x51515151 surfacing as cpi.max_lun=0x51515150 after the real kfree+reclaim (S2); UAF READ/leak via isc_dump_stats exporting seven planted freed-chunk fields through SYSCTL_OUT (S3), the same handler unprivileged users invoke; UAF WRITE via scsi_encap-isc_qout-i_nqueue_csnd and dwl writing stats.ncsnd, csnd.tqh_first, target_lun[0]/target_nluns into the freed attacker-reclaimed chunk (S4); deterministic kernel fault when the chunk holds unrelated-allocation garbage - Fatal trap 12 supervisor write at scsi_encap+0x445, the dwl indexed write sp->target_lun[sp->target_nluns++]=lun with an attacker-influenced 32-bit index - guest down in DDB (S5); and unprivileged (uid 1001) reads of the session OIDs succeeding mid-teardown (S6), so the unpriv user can be the racing reader while the session teardown itself is root-driven. Natural race S7 was not hit in the hammer runs performed (microsecond window) but the ctx_free-failure aggravator was observed live: OIDs left registered pointing at the freed chunk. fix.diff (early sessions[sid]=NULL under sc->lock, sp->refs with isc_sess_lookup/rele, locked ic_action lookup, sid-based sysctl handlers, drain before destroy, ctx_free before i_freeopt) validated A/B on the same harness: S0 bit-identical, S1 CAM_DEV_NOT_THERE, S2/S3/S4 victims see live values only while the drain holds the teardown (stop_done=0 free_seen=0 after 3s), S5 GUEST SURVIVED, S6 returns no data. Privesc closed with facts: the unprivileged path is read-only (leak of fixed-offset ints, not pointers; the string handlers read a pointer i_freeopt NULLs), the write primitive rides the CAM path which requires privileged access to the iscsi bus, and the attacker-reclaim direction writes into memory the attacker already owns; realistic ceiling is unprivileged info-leak/DoS during a root-initiated teardown plus privileged-context corruption - no unpriv-to-root chain on this path.
No comments yet.