Thread (48 messages) 48 messages, 7 authors, 13d ago

Re: [PATCH 02/12] xfrm: serialize state GC with device state flush

From: Chengfeng Ye <hidden>
Date: 2026-09-14 12:14:58

On Mon, Sep 14, 2026 at 7:23 PM Steffen Klassert
[off-list ref] wrote:
Ccing Author of the patch.

On Tue, Sep 08, 2026 at 03:48:54PM -0700, Jakub Kicinski wrote:
quoted
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
xfrm: serialize state GC with device state flush

This adds a global mutex, xfrm_state_gc_mutex, taken by the state GC worker
around xfrm_state_gc_destroy() and by the deferred-device pass of
xfrm_dev_state_flush(), so the two paths no longer run xfrm_dev_state_free()
on the same state concurrently.  The KASAN slab-use-after-free reported in
xfrm_dev_state_free() from xfrm_dev_state_flush() is addressed by that
serialization.
quoted
diff --git a/net/xfrm/xfrm_state.c b/net/xfrm/xfrm_state.c
index 36a4f6793edef..de097bba803b7 100644
--- a/net/xfrm/xfrm_state.c
+++ b/net/xfrm/xfrm_state.c
@@ -226,6 +226,7 @@ static struct xfrm_state_afinfo __rcu *xfrm_state_afinfo[NPROTO];

 static DEFINE_SPINLOCK(xfrm_state_gc_lock);
 static DEFINE_SPINLOCK(xfrm_state_dev_gc_lock);
+static DEFINE_MUTEX(xfrm_state_gc_mutex);

 int __xfrm_state_delete(struct xfrm_state *x);
@@ -632,8 +633,10 @@ static void xfrm_state_gc_task(struct work_struct *work)

    synchronize_rcu();

+   mutex_lock(&xfrm_state_gc_mutex);
    hlist_for_each_entry_safe(x, tmp, &gc_list, gclist)
            xfrm_state_gc_destroy(x);
+   mutex_unlock(&xfrm_state_gc_mutex);
 }

 static enum hrtimer_restart xfrm_timer_handler(struct hrtimer *me)
@@ -1000,6 +1003,7 @@ int xfrm_dev_state_flush(struct net *net, struct net_device *dev, bool task_vali
 out:
    spin_unlock_bh(&net->xfrm.xfrm_state_lock);

+   mutex_lock(&xfrm_state_gc_mutex);
    spin_lock_bh(&xfrm_state_dev_gc_lock);
 restart_gc:
    hlist_for_each_entry_safe(x, tmp, &xfrm_state_dev_gc_list, dev_gclist) {
[Severity: High]
Is the new mutex enough to cover every xfrm_dev_state_free() caller?  There
is a third caller in the acquire path in xfrm_state_find() that takes
neither the new mutex nor RTNL:

net/xfrm/xfrm_state.c:xfrm_state_find() {
      ...
#ifdef CONFIG_XFRM_OFFLOAD
              struct xfrm_dev_offload *xso = &x->xso;

              if (xso->type == XFRM_DEV_OFFLOAD_PACKET) {
                      xfrm_dev_state_delete(x);
                      xfrm_dev_state_free(x);
              }
#endif
      ...
}

That code runs inside the rcu_read_lock() taken earlier in
xfrm_state_find() (it uses xfrm_state_hold_rcu() on the found state), so it
cannot take a sleeping mutex even if one were added there.
That just pointed me to the xfrm_dev_state_free callback of the bonding driver,
it uses a mutex. But this is a different issue.
quoted
xfrm_dev_state_delete() publishes x on the global, reference-less device GC
list while xso->dev is still set:

net/xfrm/xfrm_state.c:xfrm_dev_state_delete() {
      if (dev) {
              dev->xfrmdev_ops->xdo_dev_state_delete(dev, x);
              spin_lock_bh(&xfrm_state_dev_gc_lock);
              hlist_add_head(&x->dev_gclist, &xfrm_state_dev_gc_list);
              spin_unlock_bh(&xfrm_state_dev_gc_lock);
      }
}

so the deferred pass added here, which matches only on xso->dev == dev, can
pick up the same state and call xfrm_dev_state_free() on it in parallel:

  CPU 0 (xfrm_state_find(), packet offload, km_query() failed)
  xfrm_dev_state_delete(x)            /* x now on xfrm_state_dev_gc_list */
  xfrm_dev_state_free(x)
    dev = READ_ONCE(xso->dev)         /* non-NULL */

  CPU 1 (xfrm_dev_event() -> xfrm_dev_state_flush(), same dev)
  mutex_lock(&xfrm_state_gc_mutex)
  finds x, xso->dev == dev
  spin_unlock_bh(&xfrm_state_dev_gc_lock)
  xfrm_dev_state_free(x)
    dev = READ_ONCE(xso->dev)         /* also non-NULL */

Can both CPUs then run the driver callback and the device put twice?

net/xfrm/xfrm_state.c:xfrm_dev_state_free() {
      if (dev && dev->xfrmdev_ops) {
              spin_lock_bh(&xfrm_state_dev_gc_lock);
              if (!hlist_unhashed(&x->dev_gclist))
                      hlist_del(&x->dev_gclist);
              spin_unlock_bh(&xfrm_state_dev_gc_lock);

              if (dev->xfrmdev_ops->xdo_dev_state_free)
                      dev->xfrmdev_ops->xdo_dev_state_free(dev, x);
              WRITE_ONCE(xso->dev, NULL);
              xso->type = XFRM_DEV_OFFLOAD_UNSPECIFIED;
              netdev_put(dev, &xso->dev_tracker);
      }
}

The hlist_unhashed() test only guards the list removal, not
xdo_dev_state_free() or netdev_put(), and dev is sampled before the
spinlock, so a double driver teardown and a net_device refcount underflow
look possible.

Also, since hlist_del() is used rather than hlist_del_init(), does the
second caller see the poisoned pprev as still hashed and call __hlist_del()
again, writing through LIST_POISON2?

This is the same failure mode the commit message describes:

  "Both paths can invoke the driver callback and drop the device reference."

Would addressing the root cause the commit message names -- "The device GC
list does not hold an xfrm_state reference" -- be a better fit, either by
taking an xfrm_state reference for the device GC list, or by making the
release in xfrm_dev_state_free() idempotent (test-and-clear of xso->dev plus
hlist_del_init() under xfrm_state_dev_gc_lock)?  That would also cover the
xfrm_state_find() caller, which cannot participate in a mutex-based scheme.
This approach can indeed not solve the problem completely. I tend to
revert this one in favor of a fix with the above approach. This is
the destruction path, so a refcount would probably not harm too
much.

Chengfeng, can you have a look at this?
No problem at all, I will prepare a replacement patch with refcount
fix to completely fix the issue.

Best regards,
Chengfeng
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help