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

Re: [PATCH v6 net-next 11/15] neighbour: Convert neigh_table.entries to refcount_t.

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

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

Critical: 0 · High: 0 · Medium: 1 · Low: 1
Both are false-positive.
- [Medium] neigh_table_free() frees RCU-published, table-owned resources
  with no grace period, while the objects that point at the table are…
When arp/ND's ->exit() is called, there should be no inflight RCU
readers and we can simply rely on neigh entries' refcnt.
- [Low] Teardown ordering concern: neigh_table_clear() ends with
  neigh_table_put(), dropping the base reference installed by…
If a user is reading proc entries, netns cannot be dismantled.

--

neighbour: Convert neigh_table.entries to refcount_t.

This converts neigh_table.entries from atomic_t to refcount_t and adds
neigh_table_get()/neigh_table_put() helpers plus a new neigh_table_free()
so that the table resources are released only when the last neighbour is
gone.  neigh_table_clear() now just drops the base reference taken in
neigh_table_init().

A couple of questions about the new teardown path below.
quoted
diff --git a/net/core/neighbour.c b/net/core/neighbour.c
index 36488dbd1512..7dc8f0cdbb45 100644
--- a/net/core/neighbour.c
+++ b/net/core/neighbour.c
@@ -55,6 +55,23 @@ static void neigh_notify(struct neighbour *n, int type, int flags, u32 pid);
 static void __neigh_notify(struct neighbour *n, int type, int flags, u32 pid);
 static void pneigh_ifdown(struct neigh_table *tbl, struct net_device *dev,
                        bool skip_perm);
+static void neigh_table_free(struct neigh_table *tbl);
+
+static void neigh_table_get(struct neigh_table *tbl)
+{
+     refcount_inc(&tbl->entries);
+}
+
+static void neigh_table_put(struct neigh_table *tbl)
+{
+     if (refcount_dec_and_test(&tbl->entries))
+             neigh_table_free(tbl);
+}
+
+static int neigh_table_entries(struct neigh_table *tbl)
+{
+     return refcount_read(&tbl->entries) - 1;
+}

 #ifdef CONFIG_PROC_FS
 static const struct seq_operations neigh_stat_seq_ops;
[ ... ]
quoted
@@ -1845,6 +1862,7 @@ void neigh_table_init(struct neigh_table *tbl)
      tbl->last_flush = now;
      tbl->last_rand  = now + tbl->parms.reachable_time * 20;

+     refcount_set(&tbl->entries, 1);
      spin_lock_init(&tbl->lock);
      mutex_init(&tbl->phash_lock);
      skb_queue_head_init_class(&tbl->proxy_queue,
[ ... ]
quoted
@@ -924,7 +941,7 @@ void neigh_destroy(struct neighbour *neigh)

      neigh_dbg(2, "neigh %p is destroyed\n", neigh);

-     atomic_dec(&neigh->tbl->entries);
+     neigh_table_put(neigh->tbl);
      kfree_rcu(neigh, rcu);
 }
 EXPORT_SYMBOL(neigh_destroy);
[ ... ]
quoted
@@ -1874,6 +1892,21 @@ void neigh_table_init(struct neigh_table *tbl)
      panic("cannot allocate memory");
 }

+static void neigh_table_free(struct neigh_table *tbl)
+{
+     struct neigh_hash_table *nht;
+
+     free_percpu(tbl->stats);
+     tbl->stats = NULL;
+
+     kfree(tbl->phash_buckets);
+     tbl->phash_buckets = NULL;
+
+     nht = rcu_dereference_protected(tbl->nht, 1);
+     tbl->nht = NULL;
+     neigh_hash_free_rcu(&nht->rcu);
+}
[Severity: Medium]
Is it safe to free these RCU-published members synchronously here?
neigh_table_free() calls free_percpu(tbl->stats), kfree(tbl->phash_buckets)
and invokes the RCU callback neigh_hash_free_rcu() directly, with no
grace period in between.

At the same time neigh_destroy() drops the table reference before the
neighbour body is retired:

        neigh_table_put(neigh->tbl);
        kfree_rcu(neigh, rcu);

If that put is the last one, the table (the pointee) is reclaimed
immediately while the neighbour holding neigh->tbl survives a full grace
period.

Lockless readers hold no-ref neighbour pointers under rcu_read_lock() and
dereference the table through them, for example in __neigh_event_send():

        NEIGH_CACHE_STAT_INC(neigh->tbl, unres_discards);

which is reached from the transmit path
ip_finish_output2()/ip6_finish_output2() -> neigh_output() ->
neigh_event_send().  tbl->nht is read via rcu_dereference() in
___neigh_lookup_noref(), and tbl->phash_buckets is declared __rcu and
walked by pneigh_lookup().

The same file retires nht only after 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);
        ...
}

Note also that neigh_table_put() can now reach this free from softirq
context, since neigh_timer_handler() -> neigh_release() -> neigh_destroy()
runs long after neigh_table_clear() returned, and neigh_destroy() releases
the netdev reference before the table reference, so netdev_wait_allrefs()
does not order it either.

Most reader windows do look drained in practice (cleanup_net() runs
synchronize_rcu_expedited() before the pernet exit ops, and
unregister_netdevice_many_notify() runs synchronize_net() before the
neighbour flush), so I could not show a reader that is still in flight at
that instant.  Still, would it be better to defer this free with
call_rcu()/kfree_rcu(), or to drop the table reference from the
neighbour's own RCU callback, especially since the follow-up patch adds
kfree(tbl) to this function?
quoted
 /*
  * Only called from ndisc_cleanup(), which means this is dead code
  * because we no longer can unload IPv6 module.
@@ -1881,26 +1914,15 @@ void neigh_table_init(struct neigh_table *tbl)
 int neigh_table_clear(struct neigh_table *tbl)
 {
      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_shutdown_sync(&tbl->proxy_timer);

      neigh_ifdown(tbl, NULL);
-     DEBUG_NET_WARN_ON_ONCE(atomic_read(&tbl->entries));
-
      remove_proc_entry(tbl->id, net->proc_net_stat);

-     free_percpu(tbl->stats);
-     tbl->stats = NULL;
-
-     kfree(tbl->phash_buckets);
-     tbl->phash_buckets = NULL;
-
-     nht = rcu_dereference_protected(tbl->nht, 1);
-     tbl->nht = NULL;
-     neigh_hash_free_rcu(&nht->rcu);
+     neigh_table_put(tbl);

      return 0;
 }
[Severity: Low]
This is more of an ordering question.  neigh_table_clear() drops the base
reference installed by refcount_set(&tbl->entries, 1) in
neigh_table_init(), but at this commit the table is still published: in
ndisc_cleanup() the call order is neigh_table_clear(&nd_tbl) followed by
unregister_pernet_subsys(&ndisc_net_ops).

Once the base reference is gone, neigh_table_entries() returns
refcount_read() - 1, which is entries - 1 in general and -1 when the last
neighbour is gone.  That value feeds the __u32 uAPI field:

        .ndtc_entries           = neigh_table_entries(tbl),

and the %08x in neigh_stat_seq_show(), where it would print as ffffffff.

Checking the rest of the series, this looks unreachable in the final
state: neigh_table_unregister() sets net->neigh_tables[index] = NULL
before calling neigh_table_clear(), the proc entry is removed and drained
before the put, and ndisc_cleanup() no longer calls neigh_table_clear()
at all.  At this commit the only caller is the inet6_init() error path
that the comment above already describes as dead code.

Would it still be worth dropping the base reference only after the table
is unpublished, so the biased accessor is never read without it?
quoted
@@ -2275,7 +2297,7 @@ static int neightbl_fill_info(struct sk_buff *skb, struct neigh_table *tbl,
              struct ndt_config ndc = {
                      .ndtc_key_len           = tbl->key_len,
                      .ndtc_entry_size        = tbl->entry_size,
-                     .ndtc_entries           = atomic_read(&tbl->entries),
+                     .ndtc_entries           = neigh_table_entries(tbl),
                      .ndtc_last_flush        = jiffies_to_msecs(flush_delta),
                      .ndtc_last_rand         = jiffies_to_msecs(rand_delta),
                      .ndtc_proxy_qlen        = READ_ONCE(tbl->proxy_queue.qlen),
@@ -3495,7 +3517,7 @@ static int neigh_stat_seq_show(struct seq_file *seq, void *v)
      seq_printf(seq, "%08x %08lx %08lx %08lx   %08lx %08lx %08lx   "
                      "%08lx         %08lx         %08lx         "
                      "%08lx       %08lx            %08lx\n",
-                atomic_read(&tbl->entries),
+                neigh_table_entries(tbl),

                 st->allocs,
                 st->destroys,
--
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