mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-07-22 02:17:36 -04:00
ipv6: mcast: Fix potential UAF in MLD delayed work
A race condition exists between device teardown and incoming MLD query
processing, leading to a Use-After-Free in the MLD delayed work.
During device destruction, the primary reference to inet6_dev is dropped,
which can drop its refcount to 0. The actual freeing of inet6_dev memory
is deferred via RCU.
Concurrently, the packet receive path runs under RCU read lock and obtains
the inet6_dev pointer. Because the memory is RCU-protected, CPU-0 can
safely dereference inet6_dev even if its refcount has hit 0.
However, if CPU-0 calls igmp6_event_query() and schedules delayed work, it
attempts to acquire a reference using in6_dev_hold(). This increments the
refcount from 0 to 1, triggering a "refcount_t: addition on 0" warning.
Since the inet6_dev memory is still scheduled to be freed after the RCU
grace period, the device is freed while the work is still scheduled.
When the work runs, it accesses the freed memory, causing a kernel panic.
Fix this by using refcount_inc_not_zero() (via a new helper
in6_dev_hold_safe()) to prevent acquiring a reference if the device is
already being destroyed. If the refcount is 0, we do not schedule the work.
Fixes: 1da177e4c3 ("Linux-2.6.12-rc2")
Signed-off-by: Eric Dumazet <edumazet@google.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
Link: https://patch.msgid.link/20260705181756.963063-3-edumazet@google.com
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
This commit is contained in:
committed by
Paolo Abeni
parent
7b19c0f81e
commit
9b26518b68
@@ -446,6 +446,11 @@ static inline void in6_dev_hold(struct inet6_dev *idev)
|
||||
refcount_inc(&idev->refcnt);
|
||||
}
|
||||
|
||||
static inline bool in6_dev_hold_safe(struct inet6_dev *idev)
|
||||
{
|
||||
return refcount_inc_not_zero(&idev->refcnt);
|
||||
}
|
||||
|
||||
/* called with rcu_read_lock held */
|
||||
static inline bool ip6_ignore_linkdown(const struct net_device *dev)
|
||||
{
|
||||
|
||||
@@ -1083,8 +1083,10 @@ static void mld_gq_start_work(struct inet6_dev *idev)
|
||||
mc_assert_locked(idev);
|
||||
|
||||
idev->mc_gq_running = 1;
|
||||
if (!mod_delayed_work(mld_wq, &idev->mc_gq_work, tv + 2))
|
||||
in6_dev_hold(idev);
|
||||
if (in6_dev_hold_safe(idev)) {
|
||||
if (mod_delayed_work(mld_wq, &idev->mc_gq_work, tv + 2))
|
||||
in6_dev_put(idev);
|
||||
}
|
||||
}
|
||||
|
||||
static void mld_gq_stop_work(struct inet6_dev *idev)
|
||||
@@ -1102,8 +1104,10 @@ static void mld_ifc_start_work(struct inet6_dev *idev, unsigned long delay)
|
||||
|
||||
mc_assert_locked(idev);
|
||||
|
||||
if (!mod_delayed_work(mld_wq, &idev->mc_ifc_work, tv + 2))
|
||||
in6_dev_hold(idev);
|
||||
if (in6_dev_hold_safe(idev)) {
|
||||
if (mod_delayed_work(mld_wq, &idev->mc_ifc_work, tv + 2))
|
||||
in6_dev_put(idev);
|
||||
}
|
||||
}
|
||||
|
||||
static void mld_ifc_stop_work(struct inet6_dev *idev)
|
||||
@@ -1121,8 +1125,10 @@ static void mld_dad_start_work(struct inet6_dev *idev, unsigned long delay)
|
||||
|
||||
mc_assert_locked(idev);
|
||||
|
||||
if (!mod_delayed_work(mld_wq, &idev->mc_dad_work, tv + 2))
|
||||
in6_dev_hold(idev);
|
||||
if (in6_dev_hold_safe(idev)) {
|
||||
if (mod_delayed_work(mld_wq, &idev->mc_dad_work, tv + 2))
|
||||
in6_dev_put(idev);
|
||||
}
|
||||
}
|
||||
|
||||
static void mld_dad_stop_work(struct inet6_dev *idev)
|
||||
@@ -1395,18 +1401,23 @@ static void mld_process_v2(struct inet6_dev *idev, struct mld2_query *mld,
|
||||
void igmp6_event_query(struct sk_buff *skb)
|
||||
{
|
||||
struct inet6_dev *idev = __in6_dev_get(skb->dev);
|
||||
bool put = false;
|
||||
|
||||
if (!idev || idev->dead)
|
||||
goto out;
|
||||
|
||||
spin_lock_bh(&idev->mc_query_lock);
|
||||
if (skb_queue_len(&idev->mc_query_queue) < MLD_MAX_SKBS) {
|
||||
if (skb_queue_len(&idev->mc_query_queue) < MLD_MAX_SKBS &&
|
||||
in6_dev_hold_safe(idev)) {
|
||||
__skb_queue_tail(&idev->mc_query_queue, skb);
|
||||
if (!mod_delayed_work(mld_wq, &idev->mc_query_work, 0))
|
||||
in6_dev_hold(idev);
|
||||
if (mod_delayed_work(mld_wq, &idev->mc_query_work, 0))
|
||||
put = true;
|
||||
skb = NULL;
|
||||
}
|
||||
spin_unlock_bh(&idev->mc_query_lock);
|
||||
|
||||
if (put)
|
||||
in6_dev_put(idev);
|
||||
out:
|
||||
kfree_skb(skb);
|
||||
}
|
||||
@@ -1570,18 +1581,23 @@ static void mld_query_work(struct work_struct *work)
|
||||
void igmp6_event_report(struct sk_buff *skb)
|
||||
{
|
||||
struct inet6_dev *idev = __in6_dev_get(skb->dev);
|
||||
bool put = false;
|
||||
|
||||
if (!idev || idev->dead)
|
||||
goto out;
|
||||
|
||||
spin_lock_bh(&idev->mc_report_lock);
|
||||
if (skb_queue_len(&idev->mc_report_queue) < MLD_MAX_SKBS) {
|
||||
if (skb_queue_len(&idev->mc_report_queue) < MLD_MAX_SKBS &&
|
||||
in6_dev_hold_safe(idev)) {
|
||||
__skb_queue_tail(&idev->mc_report_queue, skb);
|
||||
if (!mod_delayed_work(mld_wq, &idev->mc_report_work, 0))
|
||||
in6_dev_hold(idev);
|
||||
if (mod_delayed_work(mld_wq, &idev->mc_report_work, 0))
|
||||
put = true;
|
||||
skb = NULL;
|
||||
}
|
||||
spin_unlock_bh(&idev->mc_report_lock);
|
||||
|
||||
if (put)
|
||||
in6_dev_put(idev);
|
||||
out:
|
||||
kfree_skb(skb);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user