diff options
| author | Nicholas Bellinger <[email protected]> | 2017-05-24 21:47:09 -0700 | 
|---|---|---|
| committer | Nicholas Bellinger <[email protected]> | 2017-05-31 15:12:31 -0700 | 
| commit | 25cdda95fda78d22d44157da15aa7ea34be3c804 (patch) | |
| tree | fd8a89f00e89c70cb8bb4798ee3857de22221694 | |
| parent | f3cdbe39b2ab0636dec0d5d43b54f1061ce7566c (diff) | |
iscsi-target: Fix initial login PDU asynchronous socket close OOPs
This patch fixes a OOPs originally introduced by:
   commit bb048357dad6d604520c91586334c9c230366a14
   Author: Nicholas Bellinger <[email protected]>
   Date:   Thu Sep 5 14:54:04 2013 -0700
   iscsi-target: Add sk->sk_state_change to cleanup after TCP failure
which would trigger a NULL pointer dereference when a TCP connection
was closed asynchronously via iscsi_target_sk_state_change(), but only
when the initial PDU processing in iscsi_target_do_login() from iscsi_np
process context was blocked waiting for backend I/O to complete.
To address this issue, this patch makes the following changes.
First, it introduces some common helper functions used for checking
socket closing state, checking login_flags, and atomically checking
socket closing state + setting login_flags.
Second, it introduces a LOGIN_FLAGS_INITIAL_PDU bit to know when a TCP
connection has dropped via iscsi_target_sk_state_change(), but the
initial PDU processing within iscsi_target_do_login() in iscsi_np
context is still running.  For this case, it sets LOGIN_FLAGS_CLOSED,
but doesn't invoke schedule_delayed_work().
The original NULL pointer dereference case reported by MNC is now handled
by iscsi_target_do_login() doing a iscsi_target_sk_check_close() before
transitioning to FFP to determine when the socket has already closed,
or iscsi_target_start_negotiation() if the login needs to exchange
more PDUs (eg: iscsi_target_do_login returned 0) but the socket has
closed.  For both of these cases, the cleanup up of remaining connection
resources will occur in iscsi_target_start_negotiation() from iscsi_np
process context once the failure is detected.
Finally, to handle to case where iscsi_target_sk_state_change() is
called after the initial PDU procesing is complete, it now invokes
conn->login_work -> iscsi_target_do_login_rx() to perform cleanup once
existing iscsi_target_sk_check_close() checks detect connection failure.
For this case, the cleanup of remaining connection resources will occur
in iscsi_target_do_login_rx() from delayed workqueue process context
once the failure is detected.
Reported-by: Mike Christie <[email protected]>
Reviewed-by: Mike Christie <[email protected]>
Tested-by: Mike Christie <[email protected]>
Cc: Mike Christie <[email protected]>
Reported-by: Hannes Reinecke <[email protected]>
Cc: Hannes Reinecke <[email protected]>
Cc: Sagi Grimberg <[email protected]>
Cc: Varun Prakash <[email protected]>
Cc: <[email protected]> # v3.12+
Signed-off-by: Nicholas Bellinger <[email protected]>
| -rw-r--r-- | drivers/target/iscsi/iscsi_target_nego.c | 194 | ||||
| -rw-r--r-- | include/target/iscsi/iscsi_target_core.h | 1 | 
2 files changed, 133 insertions, 62 deletions
| diff --git a/drivers/target/iscsi/iscsi_target_nego.c b/drivers/target/iscsi/iscsi_target_nego.c index 7ccc9c1cbfd1..6f88b31242b0 100644 --- a/drivers/target/iscsi/iscsi_target_nego.c +++ b/drivers/target/iscsi/iscsi_target_nego.c @@ -493,14 +493,60 @@ static void iscsi_target_restore_sock_callbacks(struct iscsi_conn *conn)  static int iscsi_target_do_login(struct iscsi_conn *, struct iscsi_login *); -static bool iscsi_target_sk_state_check(struct sock *sk) +static bool __iscsi_target_sk_check_close(struct sock *sk)  {  	if (sk->sk_state == TCP_CLOSE_WAIT || sk->sk_state == TCP_CLOSE) { -		pr_debug("iscsi_target_sk_state_check: TCP_CLOSE_WAIT|TCP_CLOSE," +		pr_debug("__iscsi_target_sk_check_close: TCP_CLOSE_WAIT|TCP_CLOSE,"  			"returning FALSE\n"); -		return false; +		return true;  	} -	return true; +	return false; +} + +static bool iscsi_target_sk_check_close(struct iscsi_conn *conn) +{ +	bool state = false; + +	if (conn->sock) { +		struct sock *sk = conn->sock->sk; + +		read_lock_bh(&sk->sk_callback_lock); +		state = (__iscsi_target_sk_check_close(sk) || +			 test_bit(LOGIN_FLAGS_CLOSED, &conn->login_flags)); +		read_unlock_bh(&sk->sk_callback_lock); +	} +	return state; +} + +static bool iscsi_target_sk_check_flag(struct iscsi_conn *conn, unsigned int flag) +{ +	bool state = false; + +	if (conn->sock) { +		struct sock *sk = conn->sock->sk; + +		read_lock_bh(&sk->sk_callback_lock); +		state = test_bit(flag, &conn->login_flags); +		read_unlock_bh(&sk->sk_callback_lock); +	} +	return state; +} + +static bool iscsi_target_sk_check_and_clear(struct iscsi_conn *conn, unsigned int flag) +{ +	bool state = false; + +	if (conn->sock) { +		struct sock *sk = conn->sock->sk; + +		write_lock_bh(&sk->sk_callback_lock); +		state = (__iscsi_target_sk_check_close(sk) || +			 test_bit(LOGIN_FLAGS_CLOSED, &conn->login_flags)); +		if (!state) +			clear_bit(flag, &conn->login_flags); +		write_unlock_bh(&sk->sk_callback_lock); +	} +	return state;  }  static void iscsi_target_login_drop(struct iscsi_conn *conn, struct iscsi_login *login) @@ -540,6 +586,20 @@ static void iscsi_target_do_login_rx(struct work_struct *work)  	pr_debug("entering iscsi_target_do_login_rx, conn: %p, %s:%d\n",  			conn, current->comm, current->pid); +	/* +	 * If iscsi_target_do_login_rx() has been invoked by ->sk_data_ready() +	 * before initial PDU processing in iscsi_target_start_negotiation() +	 * has completed, go ahead and retry until it's cleared. +	 * +	 * Otherwise if the TCP connection drops while this is occuring, +	 * iscsi_target_start_negotiation() will detect the failure, call +	 * cancel_delayed_work_sync(&conn->login_work), and cleanup the +	 * remaining iscsi connection resources from iscsi_np process context. +	 */ +	if (iscsi_target_sk_check_flag(conn, LOGIN_FLAGS_INITIAL_PDU)) { +		schedule_delayed_work(&conn->login_work, msecs_to_jiffies(10)); +		return; +	}  	spin_lock(&tpg->tpg_state_lock);  	state = (tpg->tpg_state == TPG_STATE_ACTIVE); @@ -547,26 +607,12 @@ static void iscsi_target_do_login_rx(struct work_struct *work)  	if (!state) {  		pr_debug("iscsi_target_do_login_rx: tpg_state != TPG_STATE_ACTIVE\n"); -		iscsi_target_restore_sock_callbacks(conn); -		iscsi_target_login_drop(conn, login); -		iscsit_deaccess_np(np, tpg, tpg_np); -		return; +		goto err;  	} -	if (conn->sock) { -		struct sock *sk = conn->sock->sk; - -		read_lock_bh(&sk->sk_callback_lock); -		state = iscsi_target_sk_state_check(sk); -		read_unlock_bh(&sk->sk_callback_lock); - -		if (!state) { -			pr_debug("iscsi_target_do_login_rx, TCP state CLOSE\n"); -			iscsi_target_restore_sock_callbacks(conn); -			iscsi_target_login_drop(conn, login); -			iscsit_deaccess_np(np, tpg, tpg_np); -			return; -		} +	if (iscsi_target_sk_check_close(conn)) { +		pr_debug("iscsi_target_do_login_rx, TCP state CLOSE\n"); +		goto err;  	}  	conn->login_kworker = current; @@ -584,34 +630,29 @@ static void iscsi_target_do_login_rx(struct work_struct *work)  	flush_signals(current);  	conn->login_kworker = NULL; -	if (rc < 0) { -		iscsi_target_restore_sock_callbacks(conn); -		iscsi_target_login_drop(conn, login); -		iscsit_deaccess_np(np, tpg, tpg_np); -		return; -	} +	if (rc < 0) +		goto err;  	pr_debug("iscsi_target_do_login_rx after rx_login_io, %p, %s:%d\n",  			conn, current->comm, current->pid);  	rc = iscsi_target_do_login(conn, login);  	if (rc < 0) { -		iscsi_target_restore_sock_callbacks(conn); -		iscsi_target_login_drop(conn, login); -		iscsit_deaccess_np(np, tpg, tpg_np); +		goto err;  	} else if (!rc) { -		if (conn->sock) { -			struct sock *sk = conn->sock->sk; - -			write_lock_bh(&sk->sk_callback_lock); -			clear_bit(LOGIN_FLAGS_READ_ACTIVE, &conn->login_flags); -			write_unlock_bh(&sk->sk_callback_lock); -		} +		if (iscsi_target_sk_check_and_clear(conn, LOGIN_FLAGS_READ_ACTIVE)) +			goto err;  	} else if (rc == 1) {  		iscsi_target_nego_release(conn);  		iscsi_post_login_handler(np, conn, zero_tsih);  		iscsit_deaccess_np(np, tpg, tpg_np);  	} +	return; + +err: +	iscsi_target_restore_sock_callbacks(conn); +	iscsi_target_login_drop(conn, login); +	iscsit_deaccess_np(np, tpg, tpg_np);  }  static void iscsi_target_do_cleanup(struct work_struct *work) @@ -659,31 +700,54 @@ static void iscsi_target_sk_state_change(struct sock *sk)  		orig_state_change(sk);  		return;  	} +	state = __iscsi_target_sk_check_close(sk); +	pr_debug("__iscsi_target_sk_close_change: state: %d\n", state); +  	if (test_bit(LOGIN_FLAGS_READ_ACTIVE, &conn->login_flags)) {  		pr_debug("Got LOGIN_FLAGS_READ_ACTIVE=1 sk_state_change"  			 " conn: %p\n", conn); +		if (state) +			set_bit(LOGIN_FLAGS_CLOSED, &conn->login_flags);  		write_unlock_bh(&sk->sk_callback_lock);  		orig_state_change(sk);  		return;  	} -	if (test_and_set_bit(LOGIN_FLAGS_CLOSED, &conn->login_flags)) { +	if (test_bit(LOGIN_FLAGS_CLOSED, &conn->login_flags)) {  		pr_debug("Got LOGIN_FLAGS_CLOSED=1 sk_state_change conn: %p\n",  			 conn);  		write_unlock_bh(&sk->sk_callback_lock);  		orig_state_change(sk);  		return;  	} +	/* +	 * If the TCP connection has dropped, go ahead and set LOGIN_FLAGS_CLOSED, +	 * but only queue conn->login_work -> iscsi_target_do_login_rx() +	 * processing if LOGIN_FLAGS_INITIAL_PDU has already been cleared. +	 * +	 * When iscsi_target_do_login_rx() runs, iscsi_target_sk_check_close() +	 * will detect the dropped TCP connection from delayed workqueue context. +	 * +	 * If LOGIN_FLAGS_INITIAL_PDU is still set, which means the initial +	 * iscsi_target_start_negotiation() is running, iscsi_target_do_login() +	 * via iscsi_target_sk_check_close() or iscsi_target_start_negotiation() +	 * via iscsi_target_sk_check_and_clear() is responsible for detecting the +	 * dropped TCP connection in iscsi_np process context, and cleaning up +	 * the remaining iscsi connection resources. +	 */ +	if (state) { +		pr_debug("iscsi_target_sk_state_change got failed state\n"); +		set_bit(LOGIN_FLAGS_CLOSED, &conn->login_flags); +		state = test_bit(LOGIN_FLAGS_INITIAL_PDU, &conn->login_flags); +		write_unlock_bh(&sk->sk_callback_lock); -	state = iscsi_target_sk_state_check(sk); -	write_unlock_bh(&sk->sk_callback_lock); - -	pr_debug("iscsi_target_sk_state_change: state: %d\n", state); +		orig_state_change(sk); -	if (!state) { -		pr_debug("iscsi_target_sk_state_change got failed state\n"); -		schedule_delayed_work(&conn->login_cleanup_work, 0); +		if (!state) +			schedule_delayed_work(&conn->login_work, 0);  		return;  	} +	write_unlock_bh(&sk->sk_callback_lock); +  	orig_state_change(sk);  } @@ -946,6 +1010,15 @@ static int iscsi_target_do_login(struct iscsi_conn *conn, struct iscsi_login *lo  			if (iscsi_target_handle_csg_one(conn, login) < 0)  				return -1;  			if (login_rsp->flags & ISCSI_FLAG_LOGIN_TRANSIT) { +				/* +				 * Check to make sure the TCP connection has not +				 * dropped asynchronously while session reinstatement +				 * was occuring in this kthread context, before +				 * transitioning to full feature phase operation. +				 */ +				if (iscsi_target_sk_check_close(conn)) +					return -1; +  				login->tsih = conn->sess->tsih;  				login->login_complete = 1;  				iscsi_target_restore_sock_callbacks(conn); @@ -972,21 +1045,6 @@ static int iscsi_target_do_login(struct iscsi_conn *conn, struct iscsi_login *lo  		break;  	} -	if (conn->sock) { -		struct sock *sk = conn->sock->sk; -		bool state; - -		read_lock_bh(&sk->sk_callback_lock); -		state = iscsi_target_sk_state_check(sk); -		read_unlock_bh(&sk->sk_callback_lock); - -		if (!state) { -			pr_debug("iscsi_target_do_login() failed state for" -				 " conn: %p\n", conn); -			return -1; -		} -	} -  	return 0;  } @@ -1255,10 +1313,22 @@ int iscsi_target_start_negotiation(  		write_lock_bh(&sk->sk_callback_lock);  		set_bit(LOGIN_FLAGS_READY, &conn->login_flags); +		set_bit(LOGIN_FLAGS_INITIAL_PDU, &conn->login_flags);  		write_unlock_bh(&sk->sk_callback_lock);  	} - +	/* +	 * If iscsi_target_do_login returns zero to signal more PDU +	 * exchanges are required to complete the login, go ahead and +	 * clear LOGIN_FLAGS_INITIAL_PDU but only if the TCP connection +	 * is still active. +	 * +	 * Otherwise if TCP connection dropped asynchronously, go ahead +	 * and perform connection cleanup now. +	 */  	ret = iscsi_target_do_login(conn, login); +	if (!ret && iscsi_target_sk_check_and_clear(conn, LOGIN_FLAGS_INITIAL_PDU)) +		ret = -1; +  	if (ret < 0) {  		cancel_delayed_work_sync(&conn->login_work);  		cancel_delayed_work_sync(&conn->login_cleanup_work); diff --git a/include/target/iscsi/iscsi_target_core.h b/include/target/iscsi/iscsi_target_core.h index 275581d483dd..5f17fb770477 100644 --- a/include/target/iscsi/iscsi_target_core.h +++ b/include/target/iscsi/iscsi_target_core.h @@ -557,6 +557,7 @@ struct iscsi_conn {  #define LOGIN_FLAGS_READ_ACTIVE		1  #define LOGIN_FLAGS_CLOSED		2  #define LOGIN_FLAGS_READY		4 +#define LOGIN_FLAGS_INITIAL_PDU		8  	unsigned long		login_flags;  	struct delayed_work	login_work;  	struct delayed_work	login_cleanup_work; |