Thread (2 messages) flat view 2 messages, 2 authors, 1d ago

Re: [PATCH net v2] tipc: purge cong_links under the socket lock in tipc_release()

From: netdev-bot+sashiko@kernel.org
Date: 2026-09-04 11:20:31

Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider:

Critical: 0 · High: 1 · Medium: 0 · Low: 0

- [High] Incomplete fix: the commit message's central invariant — "Every
  other accessor of that list runs under the socket lock" — is factually…

--

tipc: purge cong_links under the socket lock in tipc_release()

This moves tipc_dest_list_purge(&tsk->cong_links) from after release_sock()
to before sock_orphan()/release_sock() in tipc_release(), so the purge runs
while the closing task owns the socket lock.  The stated goal is to exclude a
concurrent SOCK_WAKEUP handler that reaches tipc_dest_del() on the same list
via tipc_sk_rcv()'s spin_trylock_bh().
quoted hunk ↗ jump to hunk
diff --git a/net/tipc/socket.c b/net/tipc/socket.c
index d5d70eb230b56..cf7ee31638abf 100644
--- a/net/tipc/socket.c
+++ b/net/tipc/socket.c
@@ -647,10 +647,14 @@ static int tipc_release(struct socket *sock)
 	sk_stop_timer(sk, &sk->sk_timer);
 	tipc_sk_remove(tsk);
 
+	/* Purge under the socket lock: a SOCK_WAKEUP looked up before
+	 * tipc_sk_remove() can still reach tipc_dest_del() on this list.
+	 */
+	tipc_dest_list_purge(&tsk->cong_links);
+
 	sock_orphan(sk);
 	/* Reject any messages that accumulated in backlog queue */
 	release_sock(sk);
-	tipc_dest_list_purge(&tsk->cong_links);
 	tsk->cong_link_cnt = 0;
 	call_rcu(&tsk->rcu, tipc_sk_callback);
 	sock->sk = NULL;
[Severity: High]
The commit message says:

  "Every other accessor of that list runs under the socket lock"

Is that accurate for tipc_sk_timeout()?  In net/tipc/socket.c it touches
cong_links after dropping the socket spinlock:

net/tipc/socket.c:tipc_sk_timeout() {
	...
	bh_unlock_sock(sk);

	if (!skb_queue_empty(&list))
		rc = tipc_node_xmit(sock_net(sk), &list, pnode, tsk->portid);

	/* SYN messages may cause link congestion */
	if (rc == -ELINKCONG) {
		tipc_dest_push(&tsk->cong_links, pnode, 0);
		tsk->cong_link_cnt = 1;
	}
	sock_put(sk);
}

Here neither lock_sock() ownership nor sk->sk_lock.slock is held, and
tipc_dest_push() walks the list with tipc_dest_find() and then list_add()s,
with no internal locking:

net/tipc/name_table.c:tipc_dest_push() {
	if (tipc_dest_find(l, node, port))
		return false;

	dst = kmalloc_obj(*dst, GFP_ATOMIC);
	...
	list_add(&dst->list, l);
}

Does sk_stop_timer() on the preceding line quiesce that path?  It is the
asynchronous variant:

net/core/sock.c:sk_stop_timer() {
	if (timer_delete(timer))
		__sock_put(sk);
}

timer_delete() does not wait for a callback that is already running, and the
timer callback holds its own socket reference, so a tipc_sk_timeout() already
past bh_unlock_sock() and inside tipc_node_xmit() keeps running.

So can this interleaving happen on a connecting/established socket?

CPU0 (timer)                          CPU1 (close)
tipc_sk_timeout()
  bh_lock_sock(sk)
  sock_owned_by_user() false
  tipc_sk_retry_connect()
  bh_unlock_sock(sk)
  tipc_node_xmit()  /* -ELINKCONG */
                                      lock_sock(sk)
                                      __tipc_shutdown()
                                      sk_stop_timer()  /* nothing pending */
                                      tipc_sk_remove()
                                      tipc_dest_list_purge(&tsk->cong_links)
  tipc_dest_push(&tsk->cong_links, ...)

If CPU0's tipc_dest_find() walk interleaves with the purge's
list_for_each_entry_safe { list_del; kfree; }, does this reproduce the same
slab-use-after-free / __list_del_entry_valid_or_report signature quoted in the
changelog, only reached through the timer accessor instead of the SOCK_WAKEUP
accessor?

And if the push lands strictly after the purge, what frees that entry?
tipc_release() has no second purge after this change, tipc_sk_callback() only
does sock_put(), and tipc_sock_destruct() purges only sk_receive_queue:

net/tipc/socket.c:tipc_sk_callback() {
	sock_put(&tsk->sk);
}

net/tipc/socket.c:tipc_sock_destruct() {
	__skb_queue_purge(&sk->sk_receive_queue);
}

Does that leak the 32-byte struct tipc_dest and leave tsk->cong_link_cnt = 0
disagreeing with a still-populated list head that is freed with tsk?  The
unlocked tsk->cong_link_cnt = 1 in tipc_sk_timeout() also races the unlocked
reset after release_sock().

The unlocked timer accessor is not introduced here, but moving the purge
earlier widens the post-purge interval by sock_orphan() plus the
release_sock() backlog drain, and the safety argument rests on an invariant
this path does not satisfy.

Would it make sense either to do the timer's tipc_node_xmit() /
tipc_dest_push() / cong_link_cnt accounting under the socket lock, or to
quiesce the timer with a synchronous cancellation before purging?  Failing
that, could the changelog and the new comment be narrowed so they do not claim
every other accessor is serialised by the socket lock?

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260902111847.79955-1-juny24602%40gmail.com
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help