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);
next prev parent 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