[PATCH net v2 2/2] tipc: serialize publication purging with name table updates

From: Chengfeng Ye

Date: Thu Oct 01 2026 - 14:36:59 EST


tipc_publ_notify() walks a failed node publication list after the node lock
has been released. Its safe iterator is not protected by nametbl_lock,
which is acquired only inside tipc_publ_purge().

A concurrent withdrawal can unlink and schedule the saved next publication
for freeing. The purge iterator then advances to that removed publication.
It may access freed memory after the RCU grace period, or repeatedly follow
the self-linked binding_node before then.

The decoded causal stack is:

tipc_nametbl_remove_publ net/tipc/name_table.c:543
tipc_publ_purge net/tipc/name_distr.c:244
tipc_publ_notify net/tipc/name_distr.c:261
tipc_node_write_unlock net/tipc/node.c:425
tipc_node_link_down net/tipc/node.c:1094
tipc_node_delete_links net/tipc/node.c:1325
bearer_disable net/tipc/bearer.c:414
__tipc_nl_bearer_disable net/tipc/bearer.c:992
tipc_nl_bearer_disable net/tipc/bearer.c:1002

Move the failed node publications to a private list under nametbl_lock.
Select, unlink and purge one publication during each lock acquisition, so
no publication pointer is retained across an unlocked interval. Concurrent
withdrawals can remove entries from the private list under the same lock.

Holding the lock for the whole purge would keep bottom halves disabled
while removing every publication. Releasing it after each entry avoids
an excessive lock hold for nodes with many publications.

node_lost_contact() purges queued name-table updates before scheduling the
node-down notification. An update already dequeued by tipc_named_rcv()
holds nametbl_lock until it updates the publication list, so it completes
before the snapshot and is included. A publication accepted after the
snapshot remains on the live node list for a later contact.

Fixes: 9db9fdd1983e ("tipc: avoid to asynchronously notify subscriptions")
Cc: stable@xxxxxxxxxxxxxxx

Link: https://lore.kernel.org/netdev/20260927180806.1315902-1-nicoyip.dev@xxxxxxxxx/
Signed-off-by: Chengfeng Ye <nicoyip.dev@xxxxxxxxx>
---
net/tipc/name_distr.c | 27 ++++++++++++++++++++-------
1 file changed, 20 insertions(+), 7 deletions(-)

diff --git a/net/tipc/name_distr.c b/net/tipc/name_distr.c
index acf96562608b..9a400a1fa4d7 100644
--- a/net/tipc/name_distr.c
+++ b/net/tipc/name_distr.c
@@ -230,20 +230,16 @@ void tipc_named_node_up(struct net *net, u32 dnode, u16 capabilities)
*
* Invoked for each publication issued by a newly failed node.
* Removes publication structure from name table & deletes it.
+ * The caller must hold nametbl_lock and unlink the node subscription.
*/
static void tipc_publ_purge(struct net *net, struct publication *p)
{
- struct tipc_net *tn = tipc_net(net);
struct publication *_p;
struct tipc_uaddr ua;

tipc_uaddr(&ua, TIPC_SERVICE_RANGE, p->scope, p->sr.type,
p->sr.lower, p->sr.upper);
- spin_lock_bh(&tn->nametbl_lock);
_p = tipc_nametbl_remove_publ(net, &ua, &p->sk, p->key);
- if (_p)
- list_del_init(&_p->binding_node);
- spin_unlock_bh(&tn->nametbl_lock);
if (_p)
kfree_rcu(_p, rcu);
}
@@ -254,10 +250,27 @@ void tipc_publ_notify(struct net *net, struct list_head *nsub_list,
struct name_table *nt = tipc_name_table(net);
struct tipc_net *tn = tipc_net(net);

- struct publication *publ, *tmp;
+ struct publication *publ;
+ LIST_HEAD(purge_list);

- list_for_each_entry_safe(publ, tmp, nsub_list, binding_node)
+ spin_lock_bh(&tn->nametbl_lock);
+ /* Preserve publications learned after this node-down snapshot. */
+ list_splice_init(nsub_list, &purge_list);
+ spin_unlock_bh(&tn->nametbl_lock);
+
+ for (;;) {
+ spin_lock_bh(&tn->nametbl_lock);
+ if (list_empty(&purge_list)) {
+ spin_unlock_bh(&tn->nametbl_lock);
+ break;
+ }
+ publ = list_first_entry(&purge_list, struct publication,
+ binding_node);
+ list_del_init(&publ->binding_node);
tipc_publ_purge(net, publ);
+ spin_unlock_bh(&tn->nametbl_lock);
+ }
+
spin_lock_bh(&tn->nametbl_lock);
if (!(capabilities & TIPC_NAMED_BCAST))
nt->rc_dests--;
--
2.43.0