On Tue, Nov 04, 2025 at 11:09:08AM +0800, chuang wrote:
From 35dbc9abd8da820007391b707bd2c1a9c99ee67d Mon Sep 17 00:00:00 2001 From: Chuang Wang nashuiliang@gmail.com Date: Tue, 4 Nov 2025 02:52:11 +0000 Subject: [PATCH net] ipv4: route: Prevent rt_bind_exception() from rebinding stale fnhe
I'm unable to apply the patch:
$ b4 am -o - CACueBy7yNo4jq4HbiLXn0ez14w8CUTtTpPHmpSB-Ou6jhhNypA@mail.gmail.com | git am [...] Applying: ipv4: route: Prevent rt_bind_exception() from rebinding stale fnhe error: corrupt patch at line 10 Patch failed at 0001 ipv4: route: Prevent rt_bind_exception() from rebinding stale fnhe
Did you use git send-email?
A race condition exists between fnhe_remove_oldest() and rt_bind_exception() where a fnhe that is scheduled for removal can be rebound to a new dst.
The issue occurs when fnhe_remove_oldest() selects an fnhe (fnheX) for deletion, but before it can be flushed and freed via RCU, CPU 0 enters rt_bind_exception() and attempts to reuse the entry.
CPU 0 CPU 1 __mkroute_output() find_exception() [fnheX] update_or_create_fnhe() fnhe_remove_oldest() [fnheX] rt_bind_exception() [bind dst] RCU callback [fnheX freed, dst leak]
If rt_bind_exception() successfully binds fnheX to a new dst, the newly bound dst will never be properly freed because fnheX will soon be released by the RCU callback, leading to a permanent reference count leak on the old dst and the device.
This issue manifests as a device reference count leak and a warning in dmesg when unregistering the net device:
unregister_netdevice: waiting for ethX to become free. Usage count = N
Can you say more about how you debugged this? It seems like a very rare race condition. I expect netdevice_tracker to only show that a dst entry took a reference on the net device, but it wouldn't show who took a reference on the dst entry.
Fix this race by clearing 'oldest->fnhe_daddr' before calling fnhe_flush_routes(). Since rt_bind_exception() checks this field, setting it to zero prevents the stale fnhe from being reused and bound to a new dst just before it is freed.
Seems safe given that both fnhe_remove_oldest() and rt_bind_exception() access 'fnhe_daddr' while holding 'fnhe_lock'. Same trick was used in commit ee60ad219f5c ("route: set the deleted fnhe fnhe_daddr to 0 in ip_del_fnhe to fix a race").
Cc: stable@vger.kernel.org Fixes: 67d6d681e15b ("ipv4: make exception cache less predictible") Signed-off-by: Chuang Wang nashuiliang@gmail.com
net/ipv4/route.c | 5 +++++ 1 file changed, 5 insertions(+)
diff --git a/net/ipv4/route.c b/net/ipv4/route.c index 6d27d3610c1c..b549d6a57307 100644 --- a/net/ipv4/route.c +++ b/net/ipv4/route.c @@ -607,6 +607,11 @@ static void fnhe_remove_oldest(struct fnhe_hash_bucket *hash) oldest_p = fnhe_p; } }
/* Clear oldest->fnhe_daddr to prevent this fnhe from being* rebound with new dsts in rt_bind_exception().*/oldest->fnhe_daddr = 0; fnhe_flush_routes(oldest); *oldest_p = oldest->fnhe_next; kfree_rcu(oldest, rcu);--