Message ID | 20210105211743.8404-1-tparkin@katalix.com |
---|---|
State | New |
Headers | show |
Series | ppp: fix refcount underflow on channel unbridge | expand |
On Tue, Jan 05, 2021 at 09:17:43PM +0000, Tom Parkin wrote: > err_unset: > write_lock_bh(&pch->upl); > - RCU_INIT_POINTER(pch->bridge, NULL); > + /* Re-check pch->bridge with upl held since a racing unbridge might already > + * have cleared it and dropped the reference on pch->bridge. > + */ > + if (rcu_dereference_protected(pch->bridge, lockdep_is_held(&pch->upl))) { > + RCU_INIT_POINTER(pch->bridge, NULL); > + drop_ref = true; > + } > write_unlock_bh(&pch->upl); > synchronize_rcu(); > + > + if (drop_ref) > + if (refcount_dec_and_test(&pchb->file.refcnt)) > + ppp_destroy_channel(pchb); > + I think this works because ppp_mutex prevents pch->bridge from being reassigned to another channel. However, this isn't obvious when reading the code, and well, I prefer to not introduce new dependencies on ppp_mutex (otherwise we'd better go with your original patch). I think we could just save pch->bridge while we're under ->upl protection, and then drop the reference of that channel (if non-NULL): err_unset: write_lock_bh(&pch->upl); + /* Re-read pch->bridge in case it was modified concurrently */ + pchb = rcu_dereference_protected(pch->bridge, + lockdep_is_held(&pch->upl)); + RCU_INIT_POINTER(pch->bridge, NULL); write_unlock_bh(&pch->upl); synchronize_rcu(); + + if (pchb) + if (refcount_dec_and_test(&pchb->file.refcnt)) + ppp_destroy_channel(pchb); + return -EALREADY; } This way we know that pchb is the channel we were pointing to when we cleared pch->bridge. And this is also a bit simpler than using an extra boolean.
On Tue, 5 Jan 2021 21:17:43 +0000 Tom Parkin wrote: > When setting up a channel bridge, ppp_bridge_channels sets the > pch->bridge field before taking the associated reference on the bridge > file instance. > > This opens up a refcount underflow bug if ppp_bridge_channels called > via. iotcl runs concurrently with ppp_unbridge_channels executing via. > file release. > > The bug is triggered by ppp_bridge_channels taking the error path > through the 'err_unset' label. In this scenario, pch->bridge is set, > but the reference on the bridged channel will not be taken because > the function errors out. If ppp_unbridge_channels observes pch->bridge > before it is unset by the error path, it will erroneously drop the > reference on the bridged channel and cause a refcount underflow. > > To avoid this, ensure that ppp_bridge_channels holds a reference on > each channel in advance of setting the bridge pointers. Please prefix the subject with [PATCH net v3] for v3, add a fixes tag, and make sure to include you sign-off.
diff --git a/drivers/net/ppp/ppp_generic.c b/drivers/net/ppp/ppp_generic.c index 09c27f7773f9..d019fa40b494 100644 --- a/drivers/net/ppp/ppp_generic.c +++ b/drivers/net/ppp/ppp_generic.c @@ -617,12 +617,15 @@ static struct bpf_prog *compat_ppp_get_filter(struct sock_fprog32 __user *p) */ static int ppp_bridge_channels(struct channel *pch, struct channel *pchb) { + bool drop_ref = false; + write_lock_bh(&pch->upl); if (pch->ppp || rcu_dereference_protected(pch->bridge, lockdep_is_held(&pch->upl))) { write_unlock_bh(&pch->upl); return -EALREADY; } + refcount_inc(&pchb->file.refcnt); rcu_assign_pointer(pch->bridge, pchb); write_unlock_bh(&pch->upl); @@ -632,19 +635,28 @@ static int ppp_bridge_channels(struct channel *pch, struct channel *pchb) write_unlock_bh(&pchb->upl); goto err_unset; } + refcount_inc(&pch->file.refcnt); rcu_assign_pointer(pchb->bridge, pch); write_unlock_bh(&pchb->upl); - refcount_inc(&pch->file.refcnt); - refcount_inc(&pchb->file.refcnt); - return 0; err_unset: write_lock_bh(&pch->upl); - RCU_INIT_POINTER(pch->bridge, NULL); + /* Re-check pch->bridge with upl held since a racing unbridge might already + * have cleared it and dropped the reference on pch->bridge. + */ + if (rcu_dereference_protected(pch->bridge, lockdep_is_held(&pch->upl))) { + RCU_INIT_POINTER(pch->bridge, NULL); + drop_ref = true; + } write_unlock_bh(&pch->upl); synchronize_rcu(); + + if (drop_ref) + if (refcount_dec_and_test(&pchb->file.refcnt)) + ppp_destroy_channel(pchb); + return -EALREADY; }