[PATCH 2/3] scsi: target: iscsi: don't attach a connection to a closing session

From: Daniel Wade

Date: Sun Nov 15 2026 - 03:59:40 EST


iscsi_login_non_zero_tsih_s2() finds a session on tpg_sess_list, skips it
if it is already marked for teardown, and takes a usage count it holds
across the whole login negotiation. That skip is sampled once, before
the negotiation, and never re-evaluated.

If the session's last connection goes away while the login is still
negotiating, iscsit_close_connection() sees ->nconn reach zero, commits
to the free under ->conn_lock and calls iscsit_close_session(), which
sleeps in iscsit_check_session_usage_count() on the reference the login
holds. iscsi_post_login_handler() then attaches the new connection,
arms its NOP-In timer and drops that reference, which is what releases the
free. conn->sess is left dangling and the timer walks the freed session:

BUG: KASAN: slab-use-after-free in iscsit_allocate_cmd+0x6de/0x780
iscsit_handle_nopin_timeout <- call_timer_fn <- run_timer_softirq
Freed by task ...: kfree <- iscsit_close_session
<- iscsit_close_connection
<- iscsit_take_action_for_connection_exit

MaxConnections does not prevent this.
iscsi_login_post_auth_non_zero_tsih() tests
(atomic_read(&sess->nconn) + 1) > MaxConnections after the reference is
taken, and ->nconn is 0 here, so (0 + 1) > 1 is false even at the
default of 1.

iscsit_close_connection() tests ->nconn under sess->conn_lock and
iscsi_post_login_handler() attaches under the same lock, so publishing the
close decision in that critical section and re-testing it in the one that
attaches makes the two mutually exclusive. ->session_close is already
that flag: only ever set, never cleared, and the logout and explicit close
branches already require it. Set it in the ERL=0 fall-back branch too and
refuse the login when it is set, dropping only the reference the login
took so iscsit_close_session() can finish the free it committed to.

The refused connection is torn down here because its kthreads were started
before the final login response went out.

Tested on mainline with KASAN: unpatched, the reproducer produces the
report above and then a general protection fault; with the series applied
the login is refused and it no longer triggers.

Fixes: e48354ce078c ("iscsi-target: Add iSCSI fabric support for target v4.1")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Daniel Wade <danjwade95@xxxxxxxxx>
---
drivers/target/iscsi/iscsi_target.c | 6 ++++
drivers/target/iscsi/iscsi_target_login.c | 41 +++++++++++++++++++++++
2 files changed, 47 insertions(+)

diff --git a/drivers/target/iscsi/iscsi_target.c b/drivers/target/iscsi/iscsi_target.c
index 124ff269b8e..ee7df7f2723 100644
--- a/drivers/target/iscsi/iscsi_target.c
+++ b/drivers/target/iscsi/iscsi_target.c
@@ -4431,6 +4431,12 @@ int iscsit_close_connection(
*/
if (!atomic_read(&sess->session_reinstatement) &&
atomic_read(&sess->session_fall_back_to_erl0)) {
+ /*
+ * Publish the close decision under ->conn_lock so a non-leading
+ * login attaching in iscsi_post_login_handler() sees it. The
+ * branches below already require ->session_close.
+ */
+ atomic_set(&sess->session_close, 1);
spin_unlock_bh(&sess->conn_lock);
complete_all(&sess->session_wait_comp);
iscsit_close_session(sess, true);
diff --git a/drivers/target/iscsi/iscsi_target_login.c b/drivers/target/iscsi/iscsi_target_login.c
index f4df5518171..de0cd95aff0 100644
--- a/drivers/target/iscsi/iscsi_target_login.c
+++ b/drivers/target/iscsi/iscsi_target_login.c
@@ -660,6 +660,33 @@ void iscsit_stop_kthreads(struct iscsit_conn *conn)
spin_unlock(&iscsit_global->ts_bitmap_lock);
}

+/*
+ * Drop a connection whose kthreads are already running but that must not enter
+ * full feature phase. conn->sess must not be touched once this returns.
+ */
+static void iscsi_post_login_fail_conn(struct iscsit_conn *conn)
+{
+ /*
+ * iscsi_target_tx_thread() only leaves cleanup to this context while
+ * conn_state is TARG_CONN_STATE_IN_LOGIN; otherwise it would reach
+ * iscsit_close_connection() and walk the session being refused.
+ */
+ conn->conn_state = TARG_CONN_STATE_IN_LOGIN;
+ iscsit_stop_kthreads(conn);
+ iscsit_dec_conn_usage_count(conn);
+
+ if (conn->sock) {
+ sock_release(conn->sock);
+ conn->sock = NULL;
+ }
+ if (conn->conn_transport->iscsit_wait_conn)
+ conn->conn_transport->iscsit_wait_conn(conn);
+ if (conn->conn_transport->iscsit_free_conn)
+ conn->conn_transport->iscsit_free_conn(conn);
+
+ iscsit_free_conn(conn);
+}
+
void iscsi_post_login_handler(
struct iscsi_np *np,
struct iscsit_conn *conn,
@@ -690,6 +717,20 @@ void iscsi_post_login_handler(
conn->param_list = NULL;

spin_lock_bh(&sess->conn_lock);
+ /*
+ * The skip filter in iscsi_login_non_zero_tsih_s2() sampled this
+ * before the negotiation. Re-test under the lock that serialises
+ * against the ->nconn test in iscsit_close_connection().
+ */
+ if (atomic_read(&sess->session_close)) {
+ spin_unlock_bh(&sess->conn_lock);
+ pr_err("iSCSI session is closing, failing login on CID: %u\n",
+ conn->cid);
+ iscsi_post_login_fail_conn(conn);
+ /* Last access to @sess: this lets close_session() free it. */
+ iscsit_dec_session_usage_count(sess);
+ return;
+ }
atomic_set(&sess->session_continuation, 0);
if (sess->session_state == TARG_SESS_STATE_FAILED) {
pr_debug("Moving to"
--
2.39.0.windows.2