Race condition / memory safety (kmemleak risk)
Description
The commit fixes a data race in fib6_metric_set() used to update IPv6 per-destination metrics. Before the fix, two softirq contexts could concurrently observe the default metrics pointer and allocate separate dst_metrics objects, then publish one overwriting the other's pointer, leading to a memory leak (kmemleak report) and potential memory safety issues. The patch uses READ_ONCE/WRITE_ONCE and cmpxchg to ensure only one allocation is published and to guard the metrics write.
Proof of Concept
Proof-of-concept (conceptual): On a pre-fix kernel, two softirq threads concurrently call fib6_metric_set() for the same fib6_info and same metric with different values. Both see fib6_metrics == &dst_default_metrics and allocate separate dst_metrics objects. The first thread to perform cmpxchg(&f6i->fib6_metrics, &dst_default_metrics, p) wins and publishes p; the second thread, failing the cmpxchg, frees its allocation, avoiding a leak. Prior to the fix, the losing thread's allocated dst_metrics could become unreferenced (kmemleak report). Steps to reproduce include enabling kmemleak, injecting two parallel ND Router Discovery paths that call fib6_metric_set for the same f6i, and observing a kmemleak warning for an unreferenced dst_metrics object. A minimal user-space simulation is possible by replacing f6i and skb processing with two threads racing to update a shared pointer using a compare-exchange on a pointer to a dst_metrics struct. See the following conceptual example (C-like pseudocode):
// Conceptual race simulation (not kernel code)
struct fib6_info { struct dst_metrics *fib6_metrics; };
static struct dst_metrics dst_default_metrics;
void fib6_metric_set_sim(struct fib6_info *f6i, int metric, int val) {
if (f6i == NULL) return;
if (READ_ONCE(f6i->fib6_metrics) == &dst_default_metrics) {
struct dst_metrics *p = malloc(sizeof *p);
if (!p) return;
p->metrics[metric - 1] = val;
p->refcnt = 1;
if (CMPXCHG(&f6i->fib6_metrics, &dst_default_metrics, p) == &dst_default_metrics) {
// published successfully
return;
} else {
// losing thread frees its allocation
free(p);
// fall-through: other thread will write to the published dst_metrics
}
}
struct dst_metrics *m = READ_ONCE(f6i->fib6_metrics);
WRITE_ONCE(m->metrics[metric - 1], val);
}
Two threads calling fib6_metric_set_sim(&f6i, 1, 64) concurrently would reproduce the race on pre-fix kernels. On a real kernel, the race manifests as a potential memory leak (kmemleak warning) and a data race on metrics[] before the fix.
Commit Details
Author: Hangbin Liu
Date: 2026-03-31 04:17 UTC
Message:
ipv6: fix data race in fib6_metric_set() using cmpxchg
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>
Triage Assessment
Vulnerability Type: Race condition / Memory safety
Confidence: MEDIUM
Reasoning:
The commit addresses a data race in fib6_metric_set() that could lead to a memory safety issue and a kmemleak-type vulnerability, by introducing cmpxchg-based synchronization and proper memory fencing. While not a typical authentication/authorization or crypto flaw, the race fix mitigates a security-relevant memory safety race condition.
Verification Assessment
Vulnerability Type: Race condition / memory safety (kmemleak risk)
Confidence: MEDIUM
Affected Versions: v7.0-rc6 and earlier (pre-fix versions containing this race in fib6_metric_set())
Code Diff
diff --git a/net/ipv6/ip6_fib.c b/net/ipv6/ip6_fib.c
index dd26657b6a4acd..45ef4d65dcbc70 100644
--- a/net/ipv6/ip6_fib.c
+++ b/net/ipv6/ip6_fib.c
@@ -727,20 +727,28 @@ static int inet6_dump_fib(struct sk_buff *skb, struct netlink_callback *cb)
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);
}
/*