From: Nikolay Aleksandrov <razor@blackwall.org> Date: 2021-11-29 16:10:10
From: Nikolay Aleksandrov <redacted>
Currently we have different cleanup expectations by different users of
fib6_nh_init:
1. nh_create_ipv6
- calls fib6_nh_release manually which does full cleanup
2. ip6_route_info_create
- calls fib6_info_release to drop refs to 0 and schedules rcu call
for fib6_info_destroy_rcu() which also does full cleanup
3. fib_check_nh_v6_gw
- doesn't do any cleanup on error, expects fib6_nh_init to clean up
after itself fully (nhc_pcpu_rth_output per-cpu memory leak on error)
We can alter fib6_nh_init to properly cleanup after itself so
expectations would be the same for everyone and noone would have to do
anything in such case. It is safe because the route is not inserted yet
so the fib6_nh should not be visible at fib6_nh_init point, thus it
should be possible to free up all resources in its error path. The
problems (and leaks) are because it doesn't free all resources in its
error path, the nhc_pcpu_rth_output per-cpu allocation done by
fib_nh_common_init is not cleaned up and it expects its callers to clean
up if an error occurred after it, e.g. the dst per-cpu allocation
might fail. This change allows us to fix the memory leak at 3. and also
to simplify nh_create_ipv6 and remove the special error handling.
Fixes: 717a8f5b2923 ("ipv4: Add fib_check_nh_v6_gw")
Signed-off-by: Nikolay Aleksandrov <redacted>
---
Sending as RFC to see what people think. I've tested this patch under
heavy load with replacing/traffic forwarding/nexthop add/del etc.
I've also tested error paths by adding artificial ENOMEM errors in
different fib6_nh_init stages while running kmemleak.
net/ipv4/nexthop.c | 8 +-------
net/ipv6/route.c | 15 +++++++++------
2 files changed, 10 insertions(+), 13 deletions(-)
@@ -2565,14 +2565,8 @@ static int nh_create_ipv6(struct net *net, struct nexthop *nh,/* sets nh_dev if successful */err=ipv6_stub->fib6_nh_init(net,fib6_nh,&fib6_cfg,GFP_KERNEL,extack);-if(err){-/* IPv6 is not enabled, don't call fib6_nh_release */-if(err==-EAFNOSUPPORT)-gotoout;-ipv6_stub->fib6_nh_release(fib6_nh);-}else{+if(!err)nh->nh_flags=fib6_nh->fib_nh_flags;-}out:returnerr;}
@@ -3635,7 +3635,9 @@ int fib6_nh_init(struct net *net, struct fib6_nh *fib6_nh,in6_dev_put(idev);if(err){-lwtstate_put(fib6_nh->fib_nh_lws);+/* check if we failed after fib_nh_common_init() was called */+if(fib6_nh->nh_common.nhc_pcpu_rth_output)+fib_nh_common_release(&fib6_nh->nh_common);fib6_nh->fib_nh_lws=NULL;dev_put(dev);}
@@ -2565,14 +2565,8 @@ static int nh_create_ipv6(struct net *net, struct nexthop *nh,/* sets nh_dev if successful */err=ipv6_stub->fib6_nh_init(net,fib6_nh,&fib6_cfg,GFP_KERNEL,extack);-if(err){-/* IPv6 is not enabled, don't call fib6_nh_release */-if(err==-EAFNOSUPPORT)-gotoout;-ipv6_stub->fib6_nh_release(fib6_nh);-}else{+if(!err)nh->nh_flags=fib6_nh->fib_nh_flags;-}out:returnerr;}
@@ -3635,7 +3635,9 @@ int fib6_nh_init(struct net *net, struct fib6_nh *fib6_nh,in6_dev_put(idev);if(err){-lwtstate_put(fib6_nh->fib_nh_lws);+/* check if we failed after fib_nh_common_init() was called */+if(fib6_nh->nh_common.nhc_pcpu_rth_output)+fib_nh_common_release(&fib6_nh->nh_common);fib6_nh->fib_nh_lws=NULL;dev_put(dev);}
Shouldn't this be above ip_fib_metrics_put() given nexthop_get() is
called after ip_fib_metrics_init() ?
Also, shouldn't we call fib6_nh_release() if fib6_nh_init() succeeded
and we failed later?
@@ -2565,14 +2565,8 @@ static int nh_create_ipv6(struct net *net, struct nexthop *nh,/* sets nh_dev if successful */err=ipv6_stub->fib6_nh_init(net,fib6_nh,&fib6_cfg,GFP_KERNEL,extack);-if(err){-/* IPv6 is not enabled, don't call fib6_nh_release */-if(err==-EAFNOSUPPORT)-gotoout;-ipv6_stub->fib6_nh_release(fib6_nh);-}else{+if(!err)nh->nh_flags=fib6_nh->fib_nh_flags;-}out:returnerr;}
@@ -3635,7 +3635,9 @@ int fib6_nh_init(struct net *net, struct fib6_nh *fib6_nh,in6_dev_put(idev);if(err){-lwtstate_put(fib6_nh->fib_nh_lws);+/* check if we failed after fib_nh_common_init() was called */+if(fib6_nh->nh_common.nhc_pcpu_rth_output)+fib_nh_common_release(&fib6_nh->nh_common);fib6_nh->fib_nh_lws=NULL;dev_put(dev);}
Shouldn't this be above ip_fib_metrics_put() given nexthop_get() is
called after ip_fib_metrics_init() ?
yeah, that's ok for symmetry
Also, shouldn't we call fib6_nh_release() if fib6_nh_init() succeeded
and we failed later?
Hmm, that's a clear bug. I was only looking at nexthop objects and completely
missed that there's an error possibility after the non-nexthop path.
You're correct, and in fact we have to add another error label specifically for
the non-nexthop case because we shouldn't do it in the nexthop case.
@@ -2565,14 +2565,8 @@ static int nh_create_ipv6(struct net *net, struct nexthop *nh,/* sets nh_dev if successful */err=ipv6_stub->fib6_nh_init(net,fib6_nh,&fib6_cfg,GFP_KERNEL,extack);-if(err){-/* IPv6 is not enabled, don't call fib6_nh_release */-if(err==-EAFNOSUPPORT)-gotoout;-ipv6_stub->fib6_nh_release(fib6_nh);-}else{+if(!err)nh->nh_flags=fib6_nh->fib_nh_flags;-}out:returnerr;}
This hunk looks good
agreed, but it should be a no-op now so this should be a net-next
cleanup patch.
@@ -3635,7 +3635,9 @@ int fib6_nh_init(struct net *net, struct fib6_nh *fib6_nh,in6_dev_put(idev);if(err){-lwtstate_put(fib6_nh->fib_nh_lws);+/* check if we failed after fib_nh_common_init() was called */+if(fib6_nh->nh_common.nhc_pcpu_rth_output)+fib_nh_common_release(&fib6_nh->nh_common);fib6_nh->fib_nh_lws=NULL;dev_put(dev);}
Likewise
this is a leak in the current code and should go through -net as a
separate patch.
Shouldn't this be above ip_fib_metrics_put() given nexthop_get() is
called after ip_fib_metrics_init() ?
Also, shouldn't we call fib6_nh_release() if fib6_nh_init() succeeded
and we failed later?
similarly I think this cleanup is a separate patch.
@@ -2565,14 +2565,8 @@ static int nh_create_ipv6(struct net *net, struct nexthop *nh,/* sets nh_dev if successful */err=ipv6_stub->fib6_nh_init(net,fib6_nh,&fib6_cfg,GFP_KERNEL,extack);-if(err){-/* IPv6 is not enabled, don't call fib6_nh_release */-if(err==-EAFNOSUPPORT)-gotoout;-ipv6_stub->fib6_nh_release(fib6_nh);-}else{+if(!err)nh->nh_flags=fib6_nh->fib_nh_flags;-}out:returnerr;}
This hunk looks good
agreed, but it should be a no-op now so this should be a net-next
cleanup patch.
Actually it is needed, it's not a cleanup or noop. If fib6_nh_init fails after fib_nh_common_init
in the per-cpu allocation then fib6_nh->nh_common's pointers will still be there but
freed, so it will lead to double free. We have to NULL them when freeing if we want to avoid that.
@@ -3635,7 +3635,9 @@ int fib6_nh_init(struct net *net, struct fib6_nh *fib6_nh,in6_dev_put(idev);if(err){-lwtstate_put(fib6_nh->fib_nh_lws);+/* check if we failed after fib_nh_common_init() was called */+if(fib6_nh->nh_common.nhc_pcpu_rth_output)+fib_nh_common_release(&fib6_nh->nh_common);fib6_nh->fib_nh_lws=NULL;dev_put(dev);}
Likewise
this is a leak in the current code and should go through -net as a
separate patch.
Shouldn't this be above ip_fib_metrics_put() given nexthop_get() is
called after ip_fib_metrics_init() ?
Also, shouldn't we call fib6_nh_release() if fib6_nh_init() succeeded
and we failed later?
similarly I think this cleanup is a separate patch.
Same thing, fib6_info_destroy_rcu -> fib6_nh_release would double-free the nh_common parts
if fib6_nh_init fails in the per-cpu allocation after fib_nh_common_init.
It is not a cleanup, but a result of the fix. If we want to keep it, we'll have to NULL
the nh_common parts when freeing them in fib_nh_common_release().
@@ -2565,14 +2565,8 @@ static int nh_create_ipv6(struct net *net, struct nexthop *nh,/* sets nh_dev if successful */err=ipv6_stub->fib6_nh_init(net,fib6_nh,&fib6_cfg,GFP_KERNEL,extack);-if(err){-/* IPv6 is not enabled, don't call fib6_nh_release */-if(err==-EAFNOSUPPORT)-gotoout;-ipv6_stub->fib6_nh_release(fib6_nh);-}else{+if(!err)nh->nh_flags=fib6_nh->fib_nh_flags;-}out:returnerr;}
This hunk looks good
agreed, but it should be a no-op now so this should be a net-next
cleanup patch.
Actually it is needed, it's not a cleanup or noop. If fib6_nh_init fails after fib_nh_common_init
in the per-cpu allocation then fib6_nh->nh_common's pointers will still be there but
freed, so it will lead to double free. We have to NULL them when freeing if we want to avoid that.
fib6_nh_init should do proper cleanup if it hits an error. Your bug fix
to get nhc_pcpu_rth_output freed should complete that. It can also set
the value to NULL to avoid double free on any code path.
@@ -3635,7 +3635,9 @@ int fib6_nh_init(struct net *net, struct fib6_nh *fib6_nh,in6_dev_put(idev);if(err){-lwtstate_put(fib6_nh->fib_nh_lws);+/* check if we failed after fib_nh_common_init() was called */+if(fib6_nh->nh_common.nhc_pcpu_rth_output)+fib_nh_common_release(&fib6_nh->nh_common);fib6_nh->fib_nh_lws=NULL;dev_put(dev);}
Likewise
this is a leak in the current code and should go through -net as a
separate patch.
Shouldn't this be above ip_fib_metrics_put() given nexthop_get() is
called after ip_fib_metrics_init() ?
Also, shouldn't we call fib6_nh_release() if fib6_nh_init() succeeded
and we failed later?
similarly I think this cleanup is a separate patch.
Same thing, fib6_info_destroy_rcu -> fib6_nh_release would double-free the nh_common parts
if fib6_nh_init fails in the per-cpu allocation after fib_nh_common_init.
It is not a cleanup, but a result of the fix. If we want to keep it, we'll have to NULL
the nh_common parts when freeing them in fib_nh_common_release().
exactly. set it to NULL and make the -net patch as simple as possible
@@ -2565,14 +2565,8 @@ static int nh_create_ipv6(struct net *net, struct nexthop *nh,/* sets nh_dev if successful */err=ipv6_stub->fib6_nh_init(net,fib6_nh,&fib6_cfg,GFP_KERNEL,extack);-if(err){-/* IPv6 is not enabled, don't call fib6_nh_release */-if(err==-EAFNOSUPPORT)-gotoout;-ipv6_stub->fib6_nh_release(fib6_nh);-}else{+if(!err)nh->nh_flags=fib6_nh->fib_nh_flags;-}out:returnerr;}
This hunk looks good
agreed, but it should be a no-op now so this should be a net-next
cleanup patch.
Actually it is needed, it's not a cleanup or noop. If fib6_nh_init fails after fib_nh_common_init
in the per-cpu allocation then fib6_nh->nh_common's pointers will still be there but
freed, so it will lead to double free. We have to NULL them when freeing if we want to avoid that.
fib6_nh_init should do proper cleanup if it hits an error. Your bug fix
to get nhc_pcpu_rth_output freed should complete that. It can also set
the value to NULL to avoid double free on any code path.
Indeed, that's another way of achieving the same goal.
@@ -3635,7 +3635,9 @@ int fib6_nh_init(struct net *net, struct fib6_nh *fib6_nh,in6_dev_put(idev);if(err){-lwtstate_put(fib6_nh->fib_nh_lws);+/* check if we failed after fib_nh_common_init() was called */+if(fib6_nh->nh_common.nhc_pcpu_rth_output)+fib_nh_common_release(&fib6_nh->nh_common);fib6_nh->fib_nh_lws=NULL;dev_put(dev);}
Likewise
this is a leak in the current code and should go through -net as a
separate patch.
Shouldn't this be above ip_fib_metrics_put() given nexthop_get() is
called after ip_fib_metrics_init() ?
Also, shouldn't we call fib6_nh_release() if fib6_nh_init() succeeded
and we failed later?
similarly I think this cleanup is a separate patch.
Same thing, fib6_info_destroy_rcu -> fib6_nh_release would double-free the nh_common parts
if fib6_nh_init fails in the per-cpu allocation after fib_nh_common_init.
It is not a cleanup, but a result of the fix. If we want to keep it, we'll have to NULL
the nh_common parts when freeing them in fib_nh_common_release().
exactly. set it to NULL and make the -net patch as simple as possible
Sure, of course. I'd prefer to make the code consistent w.r.t expectations regardless of
the release cycle, but I don't have a strong preference. I'll post the fix with
NULLing the nh_common pointers.
Cheers,
Nik