Merge branch 'ipv4-ipv6-fix-uaf-and-memory-leak-in-igmp-mld'

Eric Dumazet says:

====================
ipv4/ipv6: Fix UAF and memory leak in IGMP/MLD

This series addresses two potential UAF vulnerabilities
and memory leaks in the IPv4 IGMP and IPv6 MLD subsystems.

The first two patches fix a UAF where the packet receive path races with
device teardown. If the device refcount has already hit 0 (but the memory
is still held by RCU), incoming IGMP/MLD packets trying to schedule delayed
work or timers would call refcount_inc() on the 0 refcount, triggering a
warning and eventually leading to a UAF when the work runs after the device
has been freed. This is fixed by introducing safe hold helpers using
refcount_inc_not_zero(). In MLD, we also ensure we only enqueue the skb
if we successfully acquired the device reference, to avoid leaking skbs
when the device is being destroyed.

The third patch fixes memory leaks in IPv4 IGMP when timers are deleted or
stopped. When a timer is deleted (in igmp_mod_timer) or stopped (in
igmp_stop_timer) and not re-armed, the code dropped the group refcount using
refcount_dec(). However, if the group was concurrently removed from the list,
this decrement could drop the refcount to 0 without triggering the
cleanup/free path, leaking the group structure. This is fixed by using
ip_ma_put() instead, and deferring the put until after the lock is released.
====================

Link: https://patch.msgid.link/20260705181756.963063-1-edumazet@google.com
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
This commit is contained in:
Paolo Abeni
2026-07-08 14:41:04 +02:00
4 changed files with 59 additions and 19 deletions

View File

@@ -293,6 +293,11 @@ static inline void in_dev_put(struct in_device *idev)
#define __in_dev_put(idev) refcount_dec(&(idev)->refcnt)
#define in_dev_hold(idev) refcount_inc(&(idev)->refcnt)
static inline bool in_dev_hold_safe(struct in_device *idev)
{
return refcount_inc_not_zero(&idev->refcnt);
}
#endif /* __KERNEL__ */
static __inline__ __be32 inet_make_mask(int logmask)

View File

@@ -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)
{

View File

@@ -217,13 +217,18 @@ static void ip_sf_list_clear_all(struct ip_sf_list *psf)
static void igmp_stop_timer(struct ip_mc_list *im)
{
bool put = false;
spin_lock_bh(&im->lock);
if (timer_delete(&im->timer))
refcount_dec(&im->refcnt);
put = true;
WRITE_ONCE(im->tm_running, 0);
WRITE_ONCE(im->reporter, 0);
im->unsolicit_count = 0;
spin_unlock_bh(&im->lock);
if (put)
ip_ma_put(im);
}
/* It must be called with locked im->lock */
@@ -248,20 +253,26 @@ static void igmp_gq_start_timer(struct in_device *in_dev)
return;
in_dev->mr_gq_running = 1;
if (!mod_timer(&in_dev->mr_gq_timer, exp))
in_dev_hold(in_dev);
if (in_dev_hold_safe(in_dev)) {
if (mod_timer(&in_dev->mr_gq_timer, exp))
in_dev_put(in_dev);
}
}
static void igmp_ifc_start_timer(struct in_device *in_dev, int delay)
{
int tv = get_random_u32_below(delay);
if (in_dev_hold_safe(in_dev)) {
int tv = get_random_u32_below(delay);
if (!mod_timer(&in_dev->mr_ifc_timer, jiffies+tv+2))
in_dev_hold(in_dev);
if (mod_timer(&in_dev->mr_ifc_timer, jiffies + tv + 2))
in_dev_put(in_dev);
}
}
static void igmp_mod_timer(struct ip_mc_list *im, int max_delay)
{
bool put = false;
spin_lock_bh(&im->lock);
im->unsolicit_count = 0;
if (timer_delete(&im->timer)) {
@@ -271,10 +282,13 @@ static void igmp_mod_timer(struct ip_mc_list *im, int max_delay)
spin_unlock_bh(&im->lock);
return;
}
refcount_dec(&im->refcnt);
put = true;
}
igmp_start_timer(im, max_delay);
spin_unlock_bh(&im->lock);
if (put)
ip_ma_put(im);
}

View File

@@ -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);
}