]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
ipv6: fix data race in fib6_metric_set() using cmpxchg
authorHangbin Liu <liuhangbin@gmail.com>
Tue, 31 Mar 2026 04:17:18 +0000 (12:17 +0800)
committerJakub Kicinski <kuba@kernel.org>
Thu, 2 Apr 2026 00:44:35 +0000 (17:44 -0700)
fib6_metric_set() may be called concurrently from softirq context without
holding the FIB table lock. A typical path is:

  ndisc_router_discovery()
    spin_unlock_bh(&table->tb6_lock)        <- lock released
    fib6_metric_set(rt, RTAX_HOPLIMIT, ...) <- lockless call

When two CPUs process Router Advertisement packets for the same router
simultaneously, they can both arrive at fib6_metric_set() with the same
fib6_info pointer whose fib6_metrics still points to dst_default_metrics.

  if (f6i->fib6_metrics == &dst_default_metrics) {   /* both CPUs: true */
      struct dst_metrics *p = kzalloc_obj(*p, GFP_ATOMIC);
      refcount_set(&p->refcnt, 1);
      f6i->fib6_metrics = p;   /* CPU1 overwrites CPU0's p -> p0 leaked */
  }

The dst_metrics allocated by the losing CPU has refcnt=1 but no pointer
to it anywhere in memory, producing a kmemleak report:

  unreferenced object 0xff1100025aca1400 (size 96):
    comm "softirq", pid 0, jiffies 4299271239
    backtrace:
      kmalloc_trace+0x28a/0x380
      fib6_metric_set+0xcd/0x180
      ndisc_router_discovery+0x12dc/0x24b0
      icmpv6_rcv+0xc16/0x1360

Fix this by:
 - Set val for p->metrics before published via cmpxchg() so the metrics
   value is ready before the pointer becomes visible to other CPUs.
 - Replace the plain pointer store with cmpxchg() and free the allocation
   safely when competition failed.
 - Add READ_ONCE()/WRITE_ONCE() for metrics[] setting in the non-default
   metrics path to prevent compiler-based data races.

Fixes: d4ead6b34b67 ("net/ipv6: move metrics from dst to rt6_info")
Reported-by: Fei Liu <feliu@redhat.com>
Reviewed-by: Jiayuan Chen <jiayuan.chen@linux.dev>
Signed-off-by: Hangbin Liu <liuhangbin@gmail.com>
Reviewed-by: Eric Dumazet <edumazet@google.com>
Link: https://patch.msgid.link/20260331-b4-fib6_metric_set-kmemleak-v3-1-88d27f4d8825@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
net/ipv6/ip6_fib.c

index dd26657b6a4acd07b575f91e1c1252cf64693241..45ef4d65dcbc7005b8021e2da7febe84e0005537 100644 (file)
@@ -727,20 +727,28 @@ unlock:
 
 void fib6_metric_set(struct fib6_info *f6i, int metric, u32 val)
 {
+       struct dst_metrics *m;
+
        if (!f6i)
                return;
 
-       if (f6i->fib6_metrics == &dst_default_metrics) {
+       if (READ_ONCE(f6i->fib6_metrics) == &dst_default_metrics) {
+               struct dst_metrics *dflt = (struct dst_metrics *)&dst_default_metrics;
                struct dst_metrics *p = kzalloc_obj(*p, GFP_ATOMIC);
 
                if (!p)
                        return;
 
+               p->metrics[metric - 1] = val;
                refcount_set(&p->refcnt, 1);
-               f6i->fib6_metrics = p;
+               if (cmpxchg(&f6i->fib6_metrics, dflt, p) != dflt)
+                       kfree(p);
+               else
+                       return;
        }
 
-       f6i->fib6_metrics->metrics[metric - 1] = val;
+       m = READ_ONCE(f6i->fib6_metrics);
+       WRITE_ONCE(m->metrics[metric - 1], val);
 }
 
 /*