mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Joseph Qi <joseph.qi@linux.alibaba.com>
To: Cen Zhang <zzzccc427@gmail.com>
Cc: mark@fasheh.com, jlbec@evilplan.org, heming.zhao@suse.com,
	ocfs2-devel@lists.linux.dev, linux-kernel@vger.kernel.org,
	stable@vger.kernel.org, baijiaju1990@gmail.com,
	jjzuming@gmail.com
Subject: Re: [PATCH v2] ocfs2/cluster: quiesce peer slots before destroying the o2net workqueue
Date: Thu, 29 Oct 2026 19:25:12 +0800	[thread overview]
Message-ID: <76775188-2010-48ae-b92f-ed1ae819a021@linux.alibaba.com> (raw)
In-Reply-To: <pm-ocfs2-objects-candidate-0090-v7-06712049fb73524bb8d3@gmail.com>



On 10/27/26 12:32 PM, Cen Zhang wrote:
> Listener shutdown must leave no peer work behind before it destroys the
> o2net workqueue. destroy_workqueue() drains the work that is queued, but
> delayed work whose timer has not expired is invisible to it and is
> queued onto the freed workqueue when the timer fires.
> 
> o2net_stop_listening() only disconnects peers that are still in the node
> table, and o2net_disconnect_node() cancels the delayed work without
> waiting for a running instance. Several paths rearm it afterwards.
> 
> Node removal disconnects a peer before taking it out of the table. A
> heartbeat-up callback in between sees the persistent error and queues
> the connect and expiry work again, and the listener shutdown then skips
> the peer because it is no longer in the table:
> 
>   Peer removal                       Heartbeat / listener shutdown
>   ------------                       -----------------------------
>   disconnect peer; cancel work
>                                      heartbeat-up: rearm delayed work
>   remove peer from node table
>                                      stop listener
>                                        synchronize heartbeat callbacks
>                                        skip peer absent from node table
>                                        destroy o2net workqueue
>                                      delayed-work timer expires
>                                        queue on freed workqueue
> 
> o2net_connect_expired() may be running while the disconnect cancels;
> once the disconnect drops nn_lock it queues itself and the connect work
> again, and the flush does not wait for those timers.
> 
> o2net_start_connect() decides under nn_lock whether to connect, drops
> the lock to create the socket, and attaches the socket without looking
> again. A disconnect in between is undone: the socket stays attached
> with its callbacks armed, and o2net_set_nn_state() rearms the expiry
> work after it has been cancelled.
> 
> Set o2net_shutting_down for the duration of the shutdown. While it is
> set, o2net_set_nn_state() rearms no work, the connect worker repeats its
> check under nn_lock before attaching the socket and backs off, and the
> accept path refuses a new socket. With no rearm left, cancel the
> connect-expiry, connect and still-up work of every peer slot
> synchronously after the disconnect; expiry queues connect work, so it
> is cancelled first.
> 
> Those cancellations can remove the only pending connection attempt of a
> slot whose persistent error a rearm has already cleared. Leave every
> slot the way a disconnect does, with nn_timeout reset and -ENOTCONN as
> the persistent error, so that the next heartbeat-up after a restart
> reconnects and senders fail instead of waiting for a connection that
> nothing will establish.
> 
> KASAN report, only the relevant frames are kept:
> 
> BUG: KASAN: slab-use-after-free in __queue_work+0x1189/0x1600
> Read of size 4 at addr ffff8881169069c0 by task swapper/2/0
> 
> Call Trace:
>  <IRQ>
>  __queue_work+0x1189/0x1600
>  call_timer_fn+0x176/0x540
>  </IRQ>
> 
> Allocated by task 486:
>  __kmalloc_noprof+0x2de/0x780
>  __alloc_workqueue+0x61/0x17a0
>  alloc_workqueue_noprof+0xcc/0x130
>  o2net_start_listening+0x139/0x7c0
>  o2nm_node_local_store+0x4c0/0x810
>  configfs_write_iter+0x2f9/0x4f0
>  vfs_write+0x639/0x1010
>  ksys_write+0x111/0x200
> 
> Freed by task 485:
>  kfree+0x308/0x580
>  rcu_core+0x6be/0x1ed0
> 
> Last potentially related work creation:
>  __call_rcu_common.constprop.0+0x80/0x280
>  pwq_release_workfn+0x740/0xe10
> 
> The buggy address belongs to the object at ffff888116906800
>  which belongs to the cache kmalloc-1k of size 1024
> The buggy address is located 448 bytes inside of
>  freed 1024-byte region [ffff888116906800, ffff888116906c00)
> 
> Fixes: 98211489d414 ("[PATCH] OCFS2: The Second Oracle Cluster Filesystem")
> Cc: stable@vger.kernel.org
> Assisted-by: LLM
> Signed-off-by: Cen Zhang <zzzccc427@gmail.com>
> ---
> Changes in v2:
> - Add o2net_shutting_down: o2net_set_nn_state() rearms nothing while it is
>   set, the connect worker rechecks under nn_lock before attaching its socket
>   and the accept path refuses a new socket, so an in-flight connect or
>   expiry can no longer rearm the timers after the synchronous cancellations
>   (Sashiko review).
> - Leave every slot with nn_timeout reset and -ENOTCONN after the
>   cancellations, so a later heartbeat-up reconnects and senders fail instead
>   of waiting on a slot whose persistent error a rearm had cleared (Sashiko
>   review).
> - Subject changed accordingly.
> 
> Link to v1: https://lore.kernel.org/r/pm-ocfs2-objects-candidate-0090-v6-127847d463212a41b773@gmail.com
> 
>  fs/ocfs2/cluster/tcp.c | 86 +++++++++++++++++++++++++++++++++---------
>  1 file changed, 69 insertions(+), 17 deletions(-)
> 
> diff --git a/fs/ocfs2/cluster/tcp.c b/fs/ocfs2/cluster/tcp.c
> index 8b3830b601ba..eb4628839eef 100644
> --- a/fs/ocfs2/cluster/tcp.c
> +++ b/fs/ocfs2/cluster/tcp.c
> @@ -114,6 +114,8 @@ static DEFINE_MUTEX(o2net_wq_mutex);
>  static DECLARE_COMPLETION(o2net_wq_destroyed);
>  /* Heartbeat callbacks stay registered across local-node off/on. */
>  static bool o2net_listening;
> +/* set while o2net_stop_listening() quiesces the peer slots */
> +static bool o2net_shutting_down;
>  static struct work_struct o2net_listen_work;
>  
>  static struct o2hb_callback_func o2net_hb_up, o2net_hb_down;
> @@ -553,11 +555,14 @@ static void o2net_set_nn_state(struct o2net_node *nn,
>  		       SC_NODEF_ARGS(sc));
>  	}
>  
> -	/* trigger the connecting worker func as long as we're not valid,
> +	/*
> +	 * trigger the connecting worker func as long as we're not valid,
>  	 * it will back off if it shouldn't connect.  This can be called
>  	 * from node config teardown and so needs to be careful about
> -	 * the work queue actually being up. */
> -	if (!valid && o2net_wq) {
> +	 * the work queue actually being up.  Once the listener shutdown
> +	 * has started cancelling, nothing may be rearmed.
> +	 */
> +	if (!valid && o2net_wq && !READ_ONCE(o2net_shutting_down)) {
>  		unsigned long delay;
>  		/* delay if we're within a RECONNECT_DELAY of the
>  		 * last attempt */
> @@ -1578,6 +1583,23 @@ static void o2net_sc_postpone_idle(struct o2net_sock_container *sc)
>   * having a connect attempt fail, etc. This centralizes the logic which decides
>   * if a connect attempt should be made or if we should give up and all future
>   * transmit attempts should fail */
> +/*
> + * Called with nn->nn_lock held.  See if we already have one pending or
> + * have given up.  For nn_timeout, it is set when we close the connection
> + * because of the idle time out.  So it means that we have at least
> + * connected to that node successfully once, now try to connect to it
> + * again.  Nothing may connect once the listener shutdown has started.
> + */
> +static bool o2net_nn_should_connect(struct o2net_node *nn)
> +{
> +	unsigned int timeout = atomic_read(&nn->nn_timeout);
> +
> +	if (READ_ONCE(o2net_shutting_down) || nn->nn_sc)
> +		return false;
> +	return !nn->nn_persistent_error ||
> +	       (nn->nn_persistent_error == -ENOTCONN && timeout != 0);
> +}
> +
>  static void o2net_start_connect(struct work_struct *work)
>  {
>  	struct o2net_node *nn =
> @@ -1587,7 +1609,6 @@ static void o2net_start_connect(struct work_struct *work)
>  	struct socket *sock = NULL;
>  	struct sockaddr_in myaddr = {0, }, remoteaddr = {0, };
>  	int ret = 0, stop;
> -	unsigned int timeout;
>  	unsigned int nofs_flag;
>  
>  	/*
> @@ -1609,17 +1630,7 @@ static void o2net_start_connect(struct work_struct *work)
>  		goto out;
>  
>  	spin_lock(&nn->nn_lock);
> -	/*
> -	 * see if we already have one pending or have given up.
> -	 * For nn_timeout, it is set when we close the connection
> -	 * because of the idle time out. So it means that we have
> -	 * at least connected to that node successfully once,
> -	 * now try to connect to it again.
> -	 */
> -	timeout = atomic_read(&nn->nn_timeout);
> -	stop = (nn->nn_sc ||
> -		(nn->nn_persistent_error &&
> -		(nn->nn_persistent_error != -ENOTCONN || timeout == 0)));
> +	stop = !o2net_nn_should_connect(nn);
>  	spin_unlock(&nn->nn_lock);
>  	if (stop)
>  		goto out;
> @@ -1658,9 +1669,16 @@ static void o2net_start_connect(struct work_struct *work)
>  	tcp_sock_set_nodelay(sc->sc_sock->sk);
>  	tcp_sock_set_user_timeout(sock->sk, O2NET_TCP_USER_TIMEOUT);
>  
> -	o2net_register_callbacks(sc->sc_sock->sk, sc);
> -
>  	spin_lock(&nn->nn_lock);
> +	/*
> +	 * nn_lock was dropped while the socket was created.  A disconnect
> +	 * or the listener shutdown in between must not gain this socket.
> +	 */
> +	if (!o2net_nn_should_connect(nn)) {
> +		spin_unlock(&nn->nn_lock);
> +		goto out;
> +	}
> +	o2net_register_callbacks(sc->sc_sock->sk, sc);
>  	/* handshake completion will set nn->nn_sc_valid */
>  	o2net_set_nn_state(nn, sc, 0, 0);
>  	spin_unlock(&nn->nn_lock);
> @@ -1954,6 +1972,12 @@ static int o2net_accept_one(struct socket *sock, int *more)
>  	new_sock = NULL;
>  
>  	spin_lock(&nn->nn_lock);
> +	/* a slot that is being quiesced takes no new socket */
> +	if (READ_ONCE(o2net_shutting_down)) {
> +		spin_unlock(&nn->nn_lock);
> +		ret = -ESHUTDOWN;
> +		goto out;
> +	}
>  	atomic_set(&nn->nn_timeout, 0);
>  	o2net_set_nn_state(nn, sc, 0, 0);
>  	spin_unlock(&nn->nn_lock);
> @@ -2179,6 +2203,30 @@ int o2net_start_listening(struct o2nm_node *node)
>  	return ret;
>  }
>  
> +/*
> + * Called with o2net_shutting_down set.  Cancel the delayed work of a peer
> + * slot synchronously: work waiting on its timer is invisible to a flush
> + * and would be queued on the destroyed workqueue.  Then leave the slot
> + * the way a disconnect does, so that the next heartbeat-up after a
> + * restart reconnects and senders fail instead of waiting.
> + */
> +static void o2net_quiesce_nn(struct o2net_node *nn)
> +{
> +	/* whoever takes nn_lock from now on sees o2net_shutting_down */
> +	spin_lock(&nn->nn_lock);
> +	spin_unlock(&nn->nn_lock);
> +
> +	/* expiry queues connect work, so cancel it first */
> +	cancel_delayed_work_sync(&nn->nn_connect_expired);
> +	cancel_delayed_work_sync(&nn->nn_connect_work);
> +	cancel_delayed_work_sync(&nn->nn_still_up);
> +
> +	spin_lock(&nn->nn_lock);
> +	atomic_set(&nn->nn_timeout, 0);
> +	o2net_set_nn_state(nn, NULL, 0, -ENOTCONN);
> +	spin_unlock(&nn->nn_lock);
> +}

o2net_set_nn_state() still has a queue_delayed_work() that the new flag
doesn't gate. So o2net_quiesce_nn() cancels nn_still_up and afterwards
calls o2net_set_nn_state(), which queues it again.

> +
>  /* again, o2nm_this_node() doesn't work here as we're involved in
>   * tearing it down */
>  void o2net_stop_listening(struct o2nm_node *node)
> @@ -2190,6 +2238,7 @@ void o2net_stop_listening(struct o2nm_node *node)
>  	BUG_ON(o2net_listen_sock == NULL);
>  
>  	WRITE_ONCE(o2net_listening, false);
> +	WRITE_ONCE(o2net_shutting_down, true);
>  	o2hb_synchronize_callbacks();
>  
>  	/* stop the listening socket from generating work */
> @@ -2200,15 +2249,18 @@ void o2net_stop_listening(struct o2nm_node *node)
>  
>  	for (i = 0; i < ARRAY_SIZE(o2net_nodes); i++) {
>  		struct o2nm_node *node = o2nm_get_node_by_num(i);
> +
>  		if (node) {
>  			o2net_disconnect_node(node);
>  			o2nm_node_put(node);
>  		}
> +		o2net_quiesce_nn(o2net_nn_from_num(i));

o2net_idle_timer() queues the same work from softirq with no nn_lock and no
flag check, sets nn_timeout back to 1, and re-arms sc_idle_timeout — which
quiesce never deletes.

Thanks,
Joseph

>  	}
>  
>  	/* finish all work and tear down the work queue */
>  	mlog(ML_KTHREAD, "waiting for o2net thread to exit....\n");
>  	o2net_destroy_wq();
> +	WRITE_ONCE(o2net_shutting_down, false);
>  
>  	sock_release(o2net_listen_sock);
>  	o2net_listen_sock = NULL;


      reply	other threads:[~2026-10-29 11:25 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-27  4:32 Cen Zhang
2026-10-29 11:25 ` Joseph Qi [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=76775188-2010-48ae-b92f-ed1ae819a021@linux.alibaba.com \
    --to=joseph.qi@linux.alibaba.com \
    --cc=baijiaju1990@gmail.com \
    --cc=heming.zhao@suse.com \
    --cc=jjzuming@gmail.com \
    --cc=jlbec@evilplan.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=mark@fasheh.com \
    --cc=ocfs2-devel@lists.linux.dev \
    --cc=stable@vger.kernel.org \
    --cc=zzzccc427@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®