Race condition / memory safety (kmemleak risk)

MEDIUM
torvalds/linux
Commit: ffb5a4843c5b
Affected: v7.0-rc6 and earlier (pre-fix versions containing this race in fib6_metric_set())
2026-04-04 10:57 UTC

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); } /*
← Back to Alerts View on GitHub →