Thread (32 messages) flat view 32 messages, 4 authors, 1d ago

Re: [PATCH v6 net-next 10/15] neighbour: Clean up neigh_table_init() and neigh_table_clear().

From: Kuniyuki Iwashima <kuniyu@google.com>
Date: 2026-09-04 16:28:57

On Thu, Sep 3, 2026 at 8:10 PM [off-list ref] wrote:
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider:

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

- [High] net/core/neighbour.c:neigh_table_clear() now reclaims the RCU-
  published neighbour hash table synchronously: `nht =…
This is false-positive.

IPv6 is unloadable and neigh_table_clear() is not called
except for nd_init() failure, which we don't care.

Pre-existing issues:
- [Low] neigh_table_clear() (net/core/neighbour.c:1881-1906) frees table-
  wide resources (free_percpu(tbl->stats) at 1895,…

--

neighbour: Clean up neigh_table_init() and neigh_table_clear().

This reworks neigh_table_init() to allocate everything up front and unwind
on failure, and reorders neigh_table_clear() to mirror it. The teardown
side also switches to timer_shutdown_sync(), drops the explicit
pneigh_queue_purge(), replaces pr_crit() with DEBUG_NET_WARN_ON_ONCE(), and
calls neigh_hash_free_rcu() directly instead of via call_rcu().
quoted
diff --git a/net/core/neighbour.c b/net/core/neighbour.c
index 34da4cdc813d..36488dbd1512 100644
--- a/net/core/neighbour.c
+++ b/net/core/neighbour.c
[ ... neigh_table_init() hunk snipped ... ]
quoted
@@ -1869,26 +1880,27 @@ void neigh_table_init(struct neigh_table *tbl)
  */
 int neigh_table_clear(struct neigh_table *tbl)
 {
-     /* It is not clean... Fix it to unload IPv6 module safely */
+     struct net *net __maybe_unused = &init_net;
+     struct neigh_hash_table *nht;
+
      cancel_delayed_work_sync(&tbl->managed_work);
      cancel_delayed_work_sync(&tbl->gc_work);
-     timer_delete_sync(&tbl->proxy_timer);
-     pneigh_queue_purge(&tbl->proxy_queue, NULL, tbl->family);
+     timer_shutdown_sync(&tbl->proxy_timer);
+
      neigh_ifdown(tbl, NULL);
-     if (atomic_read(&tbl->entries))
-             pr_crit("neighbour leakage\n");
+     DEBUG_NET_WARN_ON_ONCE(atomic_read(&tbl->entries));
[Severity: Low]

This isn't a bug introduced by this patch, but the diagnostic swap makes a
pre-existing lifecycle problem invisible on production kernels: with
CONFIG_DEBUG_NET=n, DEBUG_NET_WARN_ON_ONCE() compiles to nothing, so the
"still have entries" condition that previously always printed is no longer
reported.

The table-owned resources are still freed unconditionally just below, and a
neighbour destroyed afterwards touches them:

net/core/neighbour.c:neigh_destroy() {
        NEIGH_CACHE_STAT_INC(neigh->tbl, destroys);
        ...
        atomic_dec(&neigh->tbl->entries);
}

The unconditional freeing itself is unchanged from the baseline, and the
next patch in the series, "neighbour: Convert neigh_table.entries to
refcount_t", defers freeing of tbl->stats, tbl->phash_buckets and tbl->nht
into neigh_table_free() once the last reference is dropped.

Is dropping the production-visible message intentional for this
intermediate commit?
quoted
-     call_rcu(&rcu_dereference_protected(tbl->nht, 1)->rcu,
-              neigh_hash_free_rcu);
-     tbl->nht = NULL;
+     remove_proc_entry(tbl->id, net->proc_net_stat);
+
+     free_percpu(tbl->stats);
+     tbl->stats = NULL;

      kfree(tbl->phash_buckets);
      tbl->phash_buckets = NULL;

-     remove_proc_entry(tbl->id, init_net.proc_net_stat);
-
-     free_percpu(tbl->stats);
-     tbl->stats = NULL;
+     nht = rcu_dereference_protected(tbl->nht, 1);
+     tbl->nht = NULL;
+     neigh_hash_free_rcu(&nht->rcu);
[Severity: High]

Can this free the neighbour hash table while lockless readers still hold a
pointer to it?

neigh_hash_free_rcu() is an RCU callback body, so calling it directly
releases both the bucket array and the table object immediately:

net/core/neighbour.c:neigh_hash_free_rcu() {
        ...
        kfree(nht->hash_heads);
        kfree(nht);
}

Nothing in the new teardown sequence waits for a grace period:
cancel_delayed_work_sync() twice, timer_shutdown_sync(), neigh_ifdown()
and remove_proc_entry(). neigh_ifdown() only takes tbl->lock, which the
readers never take.

The readers dereference tbl->nht under RCU with no NULL check:

include/net/neighbour.h:___neigh_lookup_noref() {
        struct neigh_hash_table *nht = rcu_dereference(tbl->nht);
        ...
        hash_val = hash(pkey, dev, nht->hash_rnd) >> (32 - nht->hash_shift);
        neigh_for_each_in_bucket_rcu(n, &nht->hash_heads[hash_val])
}

so a reader that already loaded nht reads hash_shift, hash_rnd and
hash_heads out of freed slab memory and then follows whatever hlist
contents are left there. neigh_lookup(), neigh_dump_table(),
neigh_seq_start() and neightbl_fill_info() have the same shape.

Is the table still published while this runs? ndisc_cleanup() calls
neigh_table_clear(&nd_tbl) before unregister_pernet_subsys(&ndisc_net_ops),
and the pernet exit path is what clears net->neigh_tables[NEIGH_ND_TABLE],
so nd_tbl remains installed in every netns during the free. An
unprivileged RTM_GETNEIGH dump reaches it holding only rcu_read_lock():

net/core/neighbour.c:neigh_dump_info() {
        rcu_read_lock();
        for (t = 0; t < NEIGH_NR_TABLES; t++) {
                tbl = net->neigh_tables[t];

and ndisc_cleanup() is live today from the module-init error unwind:

net/ipv6/af_inet6.c:inet6_init() {
igmp_fail:
        ndisc_cleanup();

The peer site retiring the same object type still uses a grace period:

net/core/neighbour.c:neigh_hash_grow() {
        rcu_assign_pointer(tbl->nht, new_nht);
        call_rcu(&old_nht->rcu, neigh_hash_free_rcu);
}

Would keeping call_rcu()/kfree_rcu(), or inserting a synchronize_net()
before the free, be preferable here? The plain NULL store into the __rcu
pointer tbl->nht predates this patch; what is new is the access to freed
memory. This also does not appear to be addressed later in the series: at
the end of the range the same unsynchronised free lives in
neigh_table_free(), now additionally doing kfree(tbl), while
neigh_table_unregister() clears net->neigh_tables[index] and calls
neigh_table_clear()/neigh_table_put() with no grace period in between.
quoted
      return 0;
 }
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260902203722.926528-1-kuniyu%40google.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