Netdev List
 help / color / mirror / Atom feed
From: Steffen Klassert <steffen.klassert@secunet.com>
To: Jakub Kicinski <kuba@kernel.org>, Zihan Xi <xizh2024@lzu.edu.cn>
Cc: <davem@davemloft.net>, <herbert@gondor.apana.org.au>,
	<netdev@vger.kernel.org>
Subject: Re: [PATCH 5/8] xfrm: avoid lock inversion in nat keepalive work
Date: Mon, 3 Aug 2026 18:58:06 +0200	[thread overview]
Message-ID: <anDIno_XWtKMM0Wc@secunet.com> (raw)
In-Reply-To: <20260731021546.789147-1-kuba@kernel.org>

Zihan, please looks at this review of your patch.

Thanks!

On Thu, Jul 30, 2026 at 07:15:46PM -0700, Jakub Kicinski wrote:
> This is an AI-generated review of your patch. The human sending this
> email has considered the AI review valid, or at least plausible.
> ---
> --- Patch [5]: [PATCH 5/8] xfrm: avoid lock inversion in nat keepalive work ---
> commit 550991d4355a9993b144d945cc62ff754d14ede8
> Author: Zihan Xi <xizh2024@lzu.edu.cn>
> 
> xfrm: avoid lock inversion in nat keepalive work
> 
> This splits nat_keepalive_work() into two phases: the xfrm_state_walk()
> callback now only collects candidate states and takes a reference on each,
> and the actual keepalive processing (which takes x->lock) happens after the
> walk has dropped net->xfrm.xfrm_state_lock.
> 
> > diff --git a/net/xfrm/xfrm_nat_keepalive.c b/net/xfrm/xfrm_nat_keepalive.c
> > index eb1b6f67739e1..8679c68c10a16 100644
> > --- a/net/xfrm/xfrm_nat_keepalive.c
> > +++ b/net/xfrm/xfrm_nat_keepalive.c
> > @@ -156,24 +156,51 @@ static void nat_keepalive_send(struct nat_keepalive *ka)
> >  }
> >  
> >  struct nat_keepalive_work_ctx {
> > +	struct list_head states;
> >  	time64_t next_run;
> >  	time64_t now;
> >  };
> >  
> > -static int nat_keepalive_work_single(struct xfrm_state *x, int count, void *ptr)
> > +struct nat_keepalive_state {
> > +	struct list_head list;
> > +	struct xfrm_state *x;
> > +};
> > +
> > +static int nat_keepalive_work_collect(struct xfrm_state *x, int count, void *ptr)
> >  {
> >  	struct nat_keepalive_work_ctx *ctx = ptr;
> > +	struct nat_keepalive_state *state;
> > +
> > +	if (!READ_ONCE(x->nat_keepalive_interval))
> > +		return 0;
> > +
> > +	state = kmalloc_obj(*state, GFP_ATOMIC);
> > +	if (!state)
> > +		return -ENOMEM;
> 
> [Severity: Medium]
> [Finding: main-1-1]
> [Sources: opus-5]
> Does the allocation footprint here scale with the number of SAs in a way
> that is a concern?  nat_keepalive_work_collect() runs as the
> xfrm_state_walk() callback, so it executes under
> spin_lock_bh(&net->xfrm.xfrm_state_lock), which forces GFP_ATOMIC.
> 
> One node is allocated from the __GFP_HIGH reserves, and one
> xfrm_state_hold() is taken, for every ESP SA with nat_keepalive_interval
> set.  On a gateway with a large number of keepalive SAs that is a single
> burst of tens or hundreds of thousands of GFP_ATOMIC allocations per
> round, and a single failure aborts the entire round (see below).
> 
> For reference, the lock hold time itself is not a regression here: the
> baseline callback already did alloc_skb(GFP_ATOMIC) and transmitted under
> the same lock, so that part is if anything improved.  The concerns are the
> new per-SA reserve draw, the new abort-on-ENOMEM failure mode, and the
> batch-wide reference pinning.
> 
> Was a non-allocating variant considered, for example READ_ONCE/WRITE_ONCE
> or a dedicated lock for the three shared fields x->lastused,
> x->nat_keepalive_interval and x->nat_keepalive_expiration, a persistent
> struct xfrm_state_walk using the walk's stop/resume support, a per-net list
> of keepalive-enabled SAs, or a bounded pre-allocated chunk?
> 
> [Severity: Low]
> [Finding: main-1-2]
> [Sources: opus-5]
> This isn't a bug, but the patch carries:
> 
>     Fixes: f531d13bdfe3 ("xfrm: support sending NAT keepalives in ESP in UDP states")
>     Cc: stable@vger.kernel.org
> 
> while using kmalloc_obj(), which is a recent addition in include/linux/slab.h:
> 
>     #define kmalloc_obj(VAR_OR_TYPE, ...) \
>         __alloc_objs(kmalloc, default_gfp(__VA_ARGS__), typeof(VAR_OR_TYPE), 1)
> 
> Stable branches containing f531d13bdfe3 do not have that macro, so the
> backport will not build without manual conversion.  Would plain
> kmalloc(sizeof(*state), GFP_ATOMIC) be preferable, since the macro buys
> nothing here?
> 
> > +
> > +	xfrm_state_hold(x);
> > +	state->x = x;
> > +	list_add_tail(&state->list, &ctx->states);
> > +	return 0;
> > +}
> > +
> > +static void nat_keepalive_work_single(struct xfrm_state *x,
> > +				      struct nat_keepalive_work_ctx *ctx)
> > +{
> >  	bool send_keepalive = false;
> >  	struct nat_keepalive ka;
> > -	time64_t next_run;
> > +	time64_t next_run = 0;
> >  	u32 interval;
> >  	int delta;
> >  
> > +	spin_lock_bh(&x->lock);
> > +
> > +	if (x->km.state == XFRM_STATE_DEAD)
> > +		goto out;
> > +
> >  	interval = x->nat_keepalive_interval;
> >  	if (!interval)
> > -		return 0;
> > -
> > -	spin_lock(&x->lock);
> > +		goto out;
> >  
> >  	delta = (int)(ctx->now - x->lastused);
> >  	if (delta < interval) {
> > @@ -187,29 +214,41 @@ static int nat_keepalive_work_single(struct xfrm_state *x, int count, void *ptr)
> >  		send_keepalive = true;
> >  	}
> >  
> > -	spin_unlock(&x->lock);
> > +out:
> > +	spin_unlock_bh(&x->lock);
> >  
> >  	if (send_keepalive)
> >  		nat_keepalive_send(&ka);
> 
> [Severity: High]
> [Finding: main-3-5]
> [Sources: opus-5]
> Is nat_keepalive_send() still called from a softirq/BH-disabled context
> after this change?  spin_unlock_bh() re-enables softirqs, and the phase-two
> drain loop in nat_keepalive_work() has no local_bh_disable(), so this call
> now runs from fully preemptible process context.
> 
> nat_keepalive_send() ends up in nat_keepalive_send_ipv4() and
> nat_keepalive_send_ipv6(), both of which do:
> 
>     local_lock_nested_bh(&nat_keepalive_sk_ipv4.bh_lock);
>     sk = this_cpu_read(nat_keepalive_sk_ipv4.sock);
>     sock_net_set(sk, net);
>     ...
>     sock_net_set(sk, &init_net);
>     local_unlock_nested_bh(&nat_keepalive_sk_ipv4.bh_lock);
> 
> and local_lock_nested_bh() starts with an assertion:
> 
>     #define __local_lock_nested_bh(lock)                    \
>         do {                                                \
>             lockdep_assert_in_softirq();                    \
>             local_lock_acquire((lock));                     \
> 
> Before this patch nat_keepalive_work_single() was itself the
> xfrm_state_walk() callback and therefore always ran with
> spin_lock_bh(&net->xfrm.xfrm_state_lock) held, so in_softirq() was true.
> 
> Does every keepalive transmission now hit that WARN_ON_ONCE, replacing the
> lockdep splat the changelog aims to fix with a different one?
> 
> Beyond the warning, is the exclusion for the per-CPU socket still there?
> On !PREEMPT_RT __local_lock_nested_bh() is lockdep bookkeeping only, so
> with BHs enabled nothing serializes the
> this_cpu_read(sock)/sock_net_set(sk, net)/xmit/sock_net_set(sk, &init_net)
> window.  Can a preempted or migrated worker let another netns' keepalive
> work clobber the same per-CPU sk, so a keepalive is emitted with the
> socket's net pointing at init_net or another netns?
> 
> On PREEMPT_RT, local_lock_nested_bh() and local_unlock_nested_bh() each
> resolve __this_cpu_local_lock() and take/release a real spinlock with no
> migrate_disable().  Without BHs disabled, can the task migrate between
> acquire and release and unlock a different CPU's lock, leaving the original
> one held forever?
> 
> Would wrapping the nat_keepalive_send(&ka) call, or the whole phase-two
> loop, in local_bh_disable()/local_bh_enable() be enough?
> 
> >  
> > -	if (!ctx->next_run || next_run < ctx->next_run)
> > +	if (next_run && (!ctx->next_run || next_run < ctx->next_run))
> >  		ctx->next_run = next_run;
> > -	return 0;
> >  }
> >  
> >  static void nat_keepalive_work(struct work_struct *work)
> >  {
> > +	struct nat_keepalive_state *state, *tmp;
> >  	struct nat_keepalive_work_ctx ctx;
> >  	struct xfrm_state_walk walk;
> >  	struct net *net;
> > +	int err;
> >  
> > +	INIT_LIST_HEAD(&ctx.states);
> >  	ctx.next_run = 0;
> >  	ctx.now = ktime_get_real_seconds();
> >  
> >  	net = container_of(work, struct net, xfrm.nat_keepalive_work.work);
> >  	xfrm_state_walk_init(&walk, IPPROTO_ESP, NULL);
> > -	xfrm_state_walk(net, &walk, nat_keepalive_work_single, &ctx);
> > +	err = xfrm_state_walk(net, &walk, nat_keepalive_work_collect, &ctx);
> >  	xfrm_state_walk_done(&walk, net);
> > +	list_for_each_entry_safe(state, tmp, &ctx.states, list) {
> > +		nat_keepalive_work_single(state->x, &ctx);
> > +		xfrm_state_put(state->x);
> > +		kfree(state);
> > +	}
> > +	if (err == -ENOMEM) {
> > +		schedule_delayed_work(&net->xfrm.nat_keepalive_work, 0);
> > +		return;
> > +	}
> 
> [Severity: High]
> [Finding: main-1-0]
> [Sources: opus-5]
> Can this retry path make forward progress?
> 
> xfrm_state_walk() aborts the walk on any non-zero callback return and only
> records a resume cursor in walk:
> 
>     err = func(state, walk->seq, data);
>     if (err) {
>         list_move_tail(&walk->all, &x->all);
>         goto out;
>     }
> 
> but xfrm_state_walk_done(&walk, net) is called unconditionally right after,
> and it does list_del(&walk->all), throwing that cursor away.  walk is a
> stack variable that xfrm_state_walk_init() re-initialises on every
> invocation.
> 
> Since WORK_STRUCT_PENDING is cleared before the handler runs, the
> schedule_delayed_work(..., 0) here succeeds and the work re-executes
> essentially immediately, restarting from the head of net->xfrm.state_all
> and failing at the same point while the atomic reserves are still
> depleted.  There is no backoff and no retry bound, and nothing is retired
> between attempts.
> 
> Does this also duplicate keepalive packets?  The send branch in
> nat_keepalive_work_single():
> 
>     } else {
>         next_run = ctx->now + interval;
>         nat_keepalive_init(&ka, x);
>         send_keepalive = true;
>     }
> 
> computes a local next_run but never updates x->nat_keepalive_expiration, so
> on each immediate retry within the same second every SA before the failure
> point still satisfies delta >= interval and
> x->nat_keepalive_expiration <= ctx->now, and sends again.
> 
> And do SAs positioned after the failure point ever get serviced, given the
> walk always restarts from the head and the ctx.next_run schedule computed
> for the states that were processed is discarded by the early return?
> 
> Would returning 0 from nat_keepalive_work_collect() to skip the
> unallocatable entry (keeping the normal ctx.next_run schedule), or resuming
> the saved walk cursor with a bounded backoff, be preferable?
> 
> The changelog describes this as a locking-order fix only and does not
> mention the new retry behaviour.  Could it be documented there?
> 
> >  	if (ctx.next_run)
> >  		schedule_delayed_work(&net->xfrm.nat_keepalive_work,
> >  				      (ctx.next_run - ctx.now) * HZ);

  reply	other threads:[~2026-08-03 16:58 UTC|newest]

Thread overview: 15+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-29  6:50 [PATCH 0/8] pull request (net): ipsec 2026-07-29 Steffen Klassert
2026-07-29  6:50 ` [PATCH 1/8] xfrm6: fix out-of-bounds write in xfrm6_input_addr() when secpath is full Steffen Klassert
2026-07-29  6:50 ` [PATCH 2/8] esp: do not unref managed frag pages in esp_ssg_unref() Steffen Klassert
2026-07-31  2:15   ` Jakub Kicinski
2026-08-03 16:56     ` Steffen Klassert
2026-07-29  6:50 ` [PATCH 3/8] xfrm: espintcp: fix UAF during close Steffen Klassert
2026-07-29  6:50 ` [PATCH 4/8] xfrm: drop ESP-in-TCP packets with no ingress device Steffen Klassert
2026-07-29  6:50 ` [PATCH 5/8] xfrm: avoid lock inversion in nat keepalive work Steffen Klassert
2026-07-31  2:15   ` Jakub Kicinski
2026-08-03 16:58     ` Steffen Klassert [this message]
2026-07-29  6:50 ` [PATCH 6/8] xfrm: Fix skb double-free in xfrm_dev_direct_output() Steffen Klassert
2026-07-29  6:50 ` [PATCH 7/8] xfrm: ah6: validate routing header segments_left Steffen Klassert
2026-07-29  6:50 ` [PATCH 8/8] xfrm: fix xfrm_state_construct() auth-trunc leak Steffen Klassert
2026-07-31  2:17 ` [PATCH 0/8] pull request (net): ipsec 2026-07-29 Jakub Kicinski
2026-08-03 16:54   ` Steffen Klassert

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=anDIno_XWtKMM0Wc@secunet.com \
    --to=steffen.klassert@secunet.com \
    --cc=davem@davemloft.net \
    --cc=herbert@gondor.apana.org.au \
    --cc=kuba@kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=xizh2024@lzu.edu.cn \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox