From: Jakub Kicinski <kuba@kernel.org>
To: steffen.klassert@secunet.com
Cc: Jakub Kicinski <kuba@kernel.org>,
davem@davemloft.net, herbert@gondor.apana.org.au,
netdev@vger.kernel.org
Subject: Re: [PATCH 08/12] xfrm: use hlist_del_init_rcu for state_cache and state_cache_input
Date: Tue, 8 Sep 2026 15:48:58 -0700 [thread overview]
Message-ID: <20260908224858.1591267-1-kuba@kernel.org> (raw)
In-Reply-To: <20260907093020.2228346-9-steffen.klassert@secunet.com>
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: use hlist_del_init_rcu for state_cache and state_cache_input
This switches the two remaining hlist_del_rcu() calls in
__xfrm_state_delete() (state_cache and state_cache_input) to
hlist_del_init_rcu(), so that all six unhash sites in that function leave
pprev == NULL. The changelog describes this as closing a write
use-after-free that in turn produces a read use-after-free in
xfrm_input_state_lookup().
There are no Link: tags in the commit header.
Two questions below, one about the changelog and one about whether the
state_cache half of the change can have any effect.
> A second __xfrm_state_delete() therefore enters __hlist_del() on the
> already-deleted state_cache/state_cache_input nodes and does
> WRITE_ONCE(*pprev, next) through LIST_POISON2 — a write use-after-free
> once the slab is reused. The corruption can in turn cause a subsequent
> hlist_for_each_entry_rcu traversal to follow a dangling next pointer,
> producing the read use-after-free reported in xfrm_input_state_lookup().
[Severity: High]
Can a second __xfrm_state_delete() reach the unhash block at all? The
whole body is gated on km.state, and the gate sets DEAD as its first
action:
net/xfrm/xfrm_state.c:__xfrm_state_delete() {
if (x->km.state != XFRM_STATE_DEAD) {
x->km.state = XFRM_STATE_DEAD;
spin_lock(&net->xfrm.xfrm_state_lock);
list_del(&x->km.all);
hlist_del_init_rcu(&x->bydst);
...
}
All four callers hold x->lock across that test-and-set:
xfrm_state_delete() spin_lock_bh(&x->lock) ... spin_unlock_bh()
xfrm_timer_handler() spin_lock(&x->lock), and also returns early
on x->km.state == XFRM_STATE_DEAD
xfrm_add_sa_expire() spin_lock_bh(&x->lock) (net/xfrm/xfrm_user.c)
xfrm_do_migrate_state() spin_lock_bh(&x->lock) (net/xfrm/xfrm_user.c)
and no path resets km.state away from DEAD. Wouldn't a second call
therefore return -ESRCH before touching any hlist?
Separately, is the described mechanism the one that would occur? If the
gate were somehow bypassed, __hlist_del()'s very first store goes through
the poisoned pprev:
include/linux/list.h:__hlist_del() {
struct hlist_node *next = n->next;
struct hlist_node **pprev = n->pprev;
WRITE_ONCE(*pprev, next);
if (next)
WRITE_ONCE(next->pprev, pprev);
}
Since hlist_del_rcu() leaves pprev == LIST_POISON2, which is a fixed
illegal address, isn't the result an immediate oops rather than "a write
use-after-free once the slab is reused"? And as the store to
next->pprev is never reached, how would this corrupt a live chain so that
a later hlist_for_each_entry_rcu() follows a dangling next pointer?
If the read use-after-free in xfrm_input_state_lookup() is real, it looks
like it has some other cause (an xfrm_state still reachable from an RCU
list after being freed), which this change does not address. Given the
two Fixes: tags and Cc: stable, could the changelog be reworded to
describe the change as consistency hardening, and could a Reported-by:,
Closes: or the KASAN splat/reproducer be included so the actual lifetime
bug can be tracked?
One more note on the stated threat model: if a double
__xfrm_state_delete() were possible, list_del(&x->km.all),
net->xfrm.state_num-- and xfrm_state_put(x) in the same body are still
non-idempotent, so would making only the hlist removals idempotent be
sufficient? Turning the poisoned-pprev write into a silent no-op also
removes the oops that would point at the real problem.
> diff --git a/net/xfrm/xfrm_state.c b/net/xfrm/xfrm_state.c
> index de097bba803b7..e45aa1ed5b965 100644
> --- a/net/xfrm/xfrm_state.c
> +++ b/net/xfrm/xfrm_state.c
> @@ -826,9 +826,9 @@ int __xfrm_state_delete(struct xfrm_state *x)
> if (!hlist_unhashed(&x->byseq))
> hlist_del_init_rcu(&x->byseq);
> if (!hlist_unhashed(&x->state_cache))
> - hlist_del_rcu(&x->state_cache);
> + hlist_del_init_rcu(&x->state_cache);
[Severity: Medium]
Can !hlist_unhashed(&x->state_cache) ever be true here? The only site in
the tree that hashes x->state_cache is at the tail of xfrm_state_find():
net/xfrm/xfrm_state.c:xfrm_state_find() {
if (x && x->km.state == XFRM_STATE_VALID && !cached &&
(!(pol->flags & XFRM_POLICY_CPU_ACQUIRE) || x->pcpu_num == pcpu_id)) {
spin_lock_bh(&net->xfrm.xfrm_state_lock);
if (hlist_unhashed(&x->state_cache))
hlist_add_head_rcu(&x->state_cache, &pol->state_cache_list);
spin_unlock_bh(&net->xfrm.xfrm_state_lock);
}
but cached is set at a label that is also the fall-through of the second
state_cache_list loop:
cached:
cached = true;
if (best)
goto found;
else if (error)
best = NULL;
The only jump to that label is the cache-hit case "if (best) goto cached;"
after the first loop, and all other paths simply fall into it, so isn't
cached unconditionally true by the time the tail check runs? That would
make the hlist_add_head_rcu(&x->state_cache, &pol->state_cache_list) call
unreachable, leave pol->state_cache_list permanently empty (the loop in
xfrm_policy_kill() then also always walks an empty list), and mean the
guard above can never be true.
The cached = true placement comes from 0045e3d80613 ("xfrm: Cache used
outbound xfrm states at the policy.") rather than from this patch, but it
appears to make the state_cache half of this change inert and contradicts
the changelog's claim that the state_cache node can be double-deleted.
Would setting cached = true only on the cache-hit branch, instead of at a
label that is also fallen into, be the right fix?
> if (!hlist_unhashed(&x->state_cache_input))
> - hlist_del_rcu(&x->state_cache_input);
> + hlist_del_init_rcu(&x->state_cache_input);
>
> if (!hlist_unhashed(&x->byspi))
> hlist_del_init_rcu(&x->byspi);
next prev parent reply other threads:[~2026-09-08 22:49 UTC|newest]
Thread overview: 34+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-07 9:29 [PATCH 0/12] pull request (net): ipsec 2026-09-07 Steffen Klassert
2026-09-07 9:29 ` [PATCH 01/12] xfrm: iptfs: fix stack OOB read in iptfs_skb_reset_frag_walk() Steffen Klassert
2026-09-08 22:48 ` Jakub Kicinski
2026-09-07 9:29 ` [PATCH 02/12] xfrm: serialize state GC with device state flush Steffen Klassert
2026-09-08 22:48 ` Jakub Kicinski
2026-09-07 9:29 ` [PATCH 03/12] xfrm: add missing RCU read lock in xfrm_send_migrate_state() Steffen Klassert
2026-09-07 9:29 ` [PATCH 04/12] xfrm: iptfs: fix runt reassembly panic from short inner tot_len Steffen Klassert
2026-09-08 22:48 ` Jakub Kicinski
2026-09-07 9:29 ` [PATCH 05/12] ipv6: xfrm: use full sockets in local error paths Steffen Klassert
2026-09-08 22:48 ` Jakub Kicinski
2026-09-07 9:29 ` [PATCH 06/12] xfrm: fix compat ALLOCSPI request use-after-free Steffen Klassert
2026-09-07 9:29 ` [PATCH 07/12] xfrm: add missing rcu_read_lock(), skb_dst_force() and dev_hold() for xfrm_trans_reinject() Steffen Klassert
2026-09-08 22:48 ` Jakub Kicinski
2026-09-07 9:29 ` [PATCH 08/12] xfrm: use hlist_del_init_rcu for state_cache and state_cache_input Steffen Klassert
2026-09-08 22:48 ` Jakub Kicinski [this message]
2026-09-07 9:29 ` [PATCH 09/12] esp: downgrade zerocopy managed frags before mutating skb frags Steffen Klassert
2026-09-08 22:49 ` Jakub Kicinski
2026-09-07 9:29 ` [PATCH 10/12] xfrm: hold net_device reference under RCU in bundle creation Steffen Klassert
2026-09-08 22:49 ` Jakub Kicinski
2026-09-07 9:29 ` [PATCH 11/12] xfrm: save input state data before secpath resets Steffen Klassert
2026-09-07 9:29 ` [PATCH 12/12] net: xfrm: reject unrepresentable espintcp transport headers Steffen Klassert
2026-09-08 22:49 ` Jakub Kicinski
2026-09-09 6:38 ` Some clarifications on the upstreaming process (was: [PATCH 0/12] pull request (net): ipsec 2026-09-07) Steffen Klassert
2026-09-09 9:23 ` Some clarifications on the upstreaming process Paolo Abeni
2026-09-09 10:22 ` Matthieu Baerts
2026-09-10 8:17 ` Steffen Klassert
2026-09-10 8:35 ` Matthieu Baerts
2026-09-10 9:28 ` Steffen Klassert
2026-09-09 10:23 ` Steffen Klassert
2026-09-09 10:34 ` Paolo Abeni
2026-09-09 10:44 ` Steffen Klassert
2026-09-09 18:57 ` Jakub Kicinski
2026-09-10 8:29 ` Matthieu Baerts
2026-09-10 9:02 ` 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=20260908224858.1591267-1-kuba@kernel.org \
--to=kuba@kernel.org \
--cc=davem@davemloft.net \
--cc=herbert@gondor.apana.org.au \
--cc=netdev@vger.kernel.org \
--cc=steffen.klassert@secunet.com \
/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