iscsi-target: Fix initial login PDU asynchronous socket close OOPs
authorNicholas Bellinger <nab@linux-iscsi.org>
Thu, 25 May 2017 04:47:09 +0000 (21:47 -0700)
committerNicholas Bellinger <nab@linux-iscsi.org>
Wed, 31 May 2017 22:12:31 +0000 (15:12 -0700)
This patch fixes a OOPs originally introduced by:

   commit bb048357dad6d604520c91586334c9c230366a14
   Author: Nicholas Bellinger <nab@linux-iscsi.org>
   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 <mchristi@redhat.com>
Reviewed-by: Mike Christie <mchristi@redhat.com>
Tested-by: Mike Christie <mchristi@redhat.com>
Cc: Mike Christie <mchristi@redhat.com>
Reported-by: Hannes Reinecke <hare@suse.com>
Cc: Hannes Reinecke <hare@suse.com>
Cc: Sagi Grimberg <sagi@grimberg.me>
Cc: Varun Prakash <varun@chelsio.com>
Cc: <stable@vger.kernel.org> # v3.12+
Signed-off-by: Nicholas Bellinger <nab@linux-iscsi.org>
drivers/target/iscsi/iscsi_target_nego.c
include/target/iscsi/iscsi_target_core.h

index 7ccc9c1..6f88b31 100644 (file)
@@ -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);
index 275581d..5f17fb7 100644 (file)
@@ -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;