[Tarantool-patches] [PATCH v3 1/1] iproto: don't destroy a session during disconnect

Konstantin Osipov kostja.osipov at gmail.com
Tue Nov 19 10:27:39 MSK 2019


* Vladislav Shpilevoy <v.shpilevoy at tarantool.org> [19/11/19 00:26]:

lgtm

> Binary session disconnect trigger yield could lead to use after
> free of the session object. That happened because iproto thread
> sent two requests to TX thread at disconnect:
> 
>     - Close the session and run its on disconnect triggers;
> 
>     - If all requests are handled, destroy the session.
> 
> When a connection is idle, all requests are handled, so both these
> requests are sent. If the first one yielded in TX thread, the
> second one arrived and destroyed the session right under the feet
> of the first one.
> 
> This can be solved in two ways - in TX thread, and in iproto
> thread.
> 
> Iproto thread solution (which is chosen in the patch): just don't
> send destroy request until disconnect returns back to iproto
> thread.
> 
> TX thread solution (alternative): add a flag which says whether
> disconnect is processed by TX. When destroy request arrives, it
> checks the flag. If disconnect is not done, the destroy request
> waits on a condition variable until it is.
> 
> The iproto is a bit tricker to implement, but it looks more
> correct.
> 
> Closes #4627
> ---
> 
> Changes in v3:
> - Flags are replaced with a state enum.
> 
> Branch: https://github.com/tarantool/tarantool/tree/gerold103/gh-4627-session-use-after-free
> Issue: https://github.com/tarantool/tarantool/issues/4627
> 
>  src/box/iproto.cc | 104 ++++++++++++++++++++++++++++++++++++----------
>  1 file changed, 83 insertions(+), 21 deletions(-)
> 
> diff --git a/src/box/iproto.cc b/src/box/iproto.cc
> index 34c8f469a..c39b8e7bf 100644
> --- a/src/box/iproto.cc
> +++ b/src/box/iproto.cc
> @@ -296,8 +296,13 @@ static const struct cmsg_hop destroy_route[] = {
>  static void
>  tx_process_disconnect(struct cmsg *m);
>  
> +/** Send destroy message to tx thread. */
> +static void
> +net_finish_disconnect(struct cmsg *m);
> +
>  static const struct cmsg_hop disconnect_route[] = {
> -	{ tx_process_disconnect, NULL }
> +	{ tx_process_disconnect, &net_pipe },
> +	{ net_finish_disconnect, NULL }
>  };
>  
>  /**
> @@ -327,6 +332,32 @@ static const struct cmsg_hop push_route[] = {
>  
>  /* {{{ iproto_connection - declaration and definition */
>  
> +/** Connection life cycle stages. */
> +enum iproto_connection_state {
> +	/**
> +	 * A connection is always alive in the beginning because
> +	 * takes an already active socket in a constructor.
> +	 */
> +	IPROTO_CONNECTION_ALIVE,
> +	/**
> +	 * Socket was closed, a notification is sent to the TX
> +	 * thread to close the session.
> +	 */
> +	IPROTO_CONNECTION_CLOSED,
> +	/**
> +	 * TX thread was notified about close, but some requests
> +	 * are still not finished. That state may be skipped in
> +	 * case the connection was already idle (not having
> +	 * unfinished requests) at the moment of closing.
> +	 */
> +	IPROTO_CONNECTION_PENDING_DESTROY,
> +	/**
> +	 * All requests are finished, a destroy request is sent to
> +	 * the TX thread.
> +	 */
> +	IPROTO_CONNECTION_DESTROYED,
> +};
> +
>  /**
>   * Context of a single client connection.
>   * Interaction scheme:
> @@ -430,8 +461,12 @@ struct iproto_connection
>  	 * connection.
>  	 */
>  	struct cmsg destroy_msg;
> -	/** True if destroy message is sent. Debug-only. */
> -	bool is_destroy_sent;
> +	/**
> +	 * Connection state. Mainly it is used to determine when
> +	 * the connection can be destroyed, and for debug purposes
> +	 * to assert on a double destroy, for example.
> +	 */
> +	enum iproto_connection_state state;
>  	struct rlist in_stop_list;
>  	/**
>  	 * Kharon is used to implement box.session.push().
> @@ -572,6 +607,35 @@ iproto_connection_stop_msg_max_limit(struct iproto_connection *con)
>  	rlist_add_tail(&stopped_connections, &con->in_stop_list);
>  }
>  
> +/**
> + * Send a destroy message to TX thread in case all requests are
> + * finished.
> + */
> +static inline void
> +iproto_connection_try_to_start_destroy(struct iproto_connection *con)
> +{
> +	assert(con->state == IPROTO_CONNECTION_CLOSED ||
> +	       con->state == IPROTO_CONNECTION_PENDING_DESTROY);
> +	if (!iproto_connection_is_idle(con)) {
> +		/*
> +		 * Not all requests are finished. Let the last
> +		 * finished request destroy the connection.
> +		 */
> +		con->state = IPROTO_CONNECTION_PENDING_DESTROY;
> +		return;
> +	}
> +	/*
> +	 * If the connection has no outstanding requests in the
> +	 * input buffer, then no one (e.g. tx thread) is referring
> +	 * to it, so it must be destroyed. Firstly queue a msg to
> +	 * destroy the session and other resources owned by TX
> +	 * thread. When it is done, iproto thread will destroy
> +	 * other parts of the connection.
> +	 */
> +	con->state = IPROTO_CONNECTION_DESTROYED;
> +	cpipe_push(&tx_pipe, &con->destroy_msg);
> +}
> +
>  /**
>   * Initiate a connection shutdown. This method may
>   * be invoked many times, and does the internal
> @@ -597,23 +661,12 @@ iproto_connection_close(struct iproto_connection *con)
>  		 */
>  		con->p_ibuf->wpos -= con->parse_size;
>  		cpipe_push(&tx_pipe, &con->disconnect_msg);
> -	}
> -	/*
> -	 * If the connection has no outstanding requests in the
> -	 * input buffer, then no one (e.g. tx thread) is referring
> -	 * to it, so it must be destroyed at once. Queue a msg to
> -	 * run on_disconnect() trigger and destroy the connection.
> -	 *
> -	 * Otherwise, it will be destroyed by the last request on
> -	 * this connection that has finished processing.
> -	 *
> -	 * The check is mandatory to not destroy a connection
> -	 * twice.
> -	 */
> -	if (iproto_connection_is_idle(con)) {
> -		assert(! con->is_destroy_sent);
> -		con->is_destroy_sent = true;
> -		cpipe_push(&tx_pipe, &con->destroy_msg);
> +		assert(con->state == IPROTO_CONNECTION_ALIVE);
> +		con->state = IPROTO_CONNECTION_CLOSED;
> +	} else if (con->state == IPROTO_CONNECTION_PENDING_DESTROY) {
> +		iproto_connection_try_to_start_destroy(con);
> +	} else {
> +		assert(con->state == IPROTO_CONNECTION_CLOSED);
>  	}
>  	rlist_del(&con->in_stop_list);
>  }
> @@ -1048,7 +1101,7 @@ iproto_connection_new(int fd)
>  	/* It may be very awkward to allocate at close. */
>  	cmsg_init(&con->destroy_msg, destroy_route);
>  	cmsg_init(&con->disconnect_msg, disconnect_route);
> -	con->is_destroy_sent = false;
> +	con->state = IPROTO_CONNECTION_ALIVE;
>  	con->tx.is_push_pending = false;
>  	con->tx.is_push_sent = false;
>  	rmean_collect(rmean_net, IPROTO_CONNECTIONS, 1);
> @@ -1063,6 +1116,7 @@ iproto_connection_delete(struct iproto_connection *con)
>  	assert(!evio_has_fd(&con->output));
>  	assert(!evio_has_fd(&con->input));
>  	assert(con->session == NULL);
> +	assert(con->state == IPROTO_CONNECTION_DESTROYED);
>  	/*
>  	 * The output buffers must have been deleted
>  	 * in tx thread.
> @@ -1281,6 +1335,14 @@ tx_process_disconnect(struct cmsg *m)
>  	}
>  }
>  
> +static void
> +net_finish_disconnect(struct cmsg *m)
> +{
> +	struct iproto_connection *con =
> +		container_of(m, struct iproto_connection, disconnect_msg);
> +	iproto_connection_try_to_start_destroy(con);
> +}
> +
>  /**
>   * Destroy the session object, as well as output buffers of the
>   * connection.
> -- 
> 2.21.0 (Apple Git-122.2)
> 

-- 
Konstantin Osipov, Moscow, Russia


More information about the Tarantool-patches mailing list