mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH net v5 0/3] xfrm: fix vti tunnel device use-after-free across async crypto resumption
@ 2026-10-19 15:32 Qihang
  2026-10-19 15:32 ` [PATCH net v5 1/3] xfrm: run the receive callback before dropping the skb->dev reference Qihang
                   ` (3 more replies)
  0 siblings, 4 replies; 8+ messages in thread
From: Qihang @ 2026-10-19 15:32 UTC (permalink / raw)
  To: netdev
  Cc: steffen.klassert, chopps, herbert, davem, edumazet, kuba, pabeni,
	horms, dsahern, idosch, linux-kernel

This replaces the earlier vti/vti6 series and the reference-transfer
rework.

vti_input() and vti6_input_proto() cache the tunnel pointer in the skb
control buffer without taking a reference. With async crypto, the
receive callback runs from crypto completion and may run after the
tunnel device has been torn down, causing a use-after-free when the
callback dereferences the cached tunnel.

The input hook takes a reference on the tunnel device when caching the
tunnel. A successful receive callback leaves the cached tunnel in place;
xfrm_input() releases the reference on every exit, including after
delivery and on drop paths. The error callback only updates the error
counters, so the common drop path releases the tunnel reference after
the skb has been freed. IPTFS, which can free or consume the outer skb
itself, releases the reference through xfrm_input_drop() or
xfrm_input_consume().

Patch 3 relies on the first two patches: the receive callback must run
before the async skb->dev reference is dropped, and the address family
must be available on the early invalid-state drop path.

Changes since v4:
- IPTFS: when an inner packet fails validation, __input_process_payload()
  can reach the drop path before the inner packets queued for delivery
  are sent. Drop those queued packets as well so the outer skb's tunnel
  device reference is not leaked (found by the Sashiko review of v4 3/3).

Changes since v3:
- the cached pointer is always a tunnel; the address family selects its
  type, and a non-NULL tunnel slot denotes one held device reference
- xfrm_input() takes the tunnel device out of the skb before
  gro_cells_receive()/transport_finish(), which may consume the skb
- the device reference is dropped only after the skb has been freed,
  including on error paths
- IPTFS passes the state's address family explicitly instead of
  inferring it from the secpath; consumed skbs keep consume_skb()
  semantics and drop paths keep their skb drop reason
- drop the xfrmi changes: it re-looks up the interface and does not
  participate in the reference transfer
- merge the reference and IPTFS changes into one patch so the series
  remains bisectable
- the gro_cells NAPI window is pre-existing and is documented, not
  changed

Runtime testing (aarch64 QEMU; KASAN for UAF detection):
- VTI async success and error: RTM_DELLINK completes without a KASAN
  report; the unfixed tree reports slab-use-after-free in vti_rcv_cb.
- Async resume with the SA deleted during the crypto window: the
  reference is released and the device is removed; with patch 2
  reverted, xfrm_input_take_tunnel_dev() warns and
  unregister_netdevice() waits for vti0.
- IPTFS outer-skb free: RTM_DELLINK completes; with the
  !first_skb xfrm_input_drop() reverted to kfree_skb(), the reference
  leaks and unregister_netdevice() waits for vti0.
- IPTFS aggregated payload with a valid first inner packet followed by
  an inner packet with an invalid IPv4 header length: RTM_DELLINK
  completes; without the sublist drain, the tunnel device reference
  leaks and unregister_netdevice() waits for vti0 (Usage count = 2).

The IPTFS timer/reorder/state-teardown paths are covered by code audit,
not by dedicated runtime triggers.

Previous versions:
  https://lore.kernel.org/netdev/20261018090638.55686-1-q.h.hack.winter@gmail.com/ (v4)
  https://lore.kernel.org/netdev/20261013074610.17808-1-q.h.hack.winter@gmail.com/ (v2)
  https://lore.kernel.org/netdev/20260930090813.73901-1-q.h.hack.winter@gmail.com/ (v1)

Qihang


Qihang (3):
  xfrm: run the receive callback before dropping the skb->dev reference
  xfrm: set the address family before the early invalid-state drop
  xfrm: hold the tunnel device reference across the input path

 net/ipv4/ip_vti.c      |  3 ++
 net/ipv6/ip6_vti.c     |  2 ++
 net/ipv6/xfrm6_input.c |  3 ++
 net/xfrm/xfrm_inout.h  |  4 +++
 net/xfrm/xfrm_input.c  | 63 +++++++++++++++++++++++++++++++++++++++++-
 net/xfrm/xfrm_iptfs.c  | 42 ++++++++++++++++++----------
 6 files changed, 105 insertions(+), 19 deletions(-)

-- 
2.54.0 (Apple Git-157)


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH net v5 1/3] xfrm: run the receive callback before dropping the skb->dev reference
  2026-10-19 15:32 [PATCH net v5 0/3] xfrm: fix vti tunnel device use-after-free across async crypto resumption Qihang
@ 2026-10-19 15:32 ` Qihang
  2026-10-22  3:34   ` netdev-bot+sashiko
  2026-10-19 15:32 ` [PATCH net v5 2/3] xfrm: set the address family before the early invalid-state drop Qihang
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 8+ messages in thread
From: Qihang @ 2026-10-19 15:32 UTC (permalink / raw)
  To: netdev
  Cc: steffen.klassert, chopps, herbert, davem, edumazet, kuba, pabeni,
	horms, dsahern, idosch, linux-kernel

The drop path in xfrm_input() releases the skb->dev reference that is
held across an async resumption before it runs the receive callbacks.
The callbacks may still dereference skb->dev, xfrmi_rcv_cb() reads
skb->dev->ifindex, so they can race a concurrent teardown of the
device.  Run the callbacks first and drop the reference afterwards,
like the success path does.

The early drop for a state that is no longer valid releases the
reference on its own; let the drop label handle that reference too.

Fixes: 1c428b038400 ("xfrm: hold dev ref until after transport_finish NF_HOOK")
Cc: stable@vger.kernel.org
Signed-off-by: Qihang <q.h.hack.winter@gmail.com>
---
 net/xfrm/xfrm_input.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/net/xfrm/xfrm_input.c b/net/xfrm/xfrm_input.c
index 5ed87d51392a..94de4f698c33 100644
--- a/net/xfrm/xfrm_input.c
+++ b/net/xfrm/xfrm_input.c
@@ -496,7 +496,7 @@ int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
 					       LINUX_MIB_XFRMINSTATEINVALID);
 
 			if (encap_type == -1)
-				dev_put(dev);
+				async = 1;
 			goto drop;
 		}
 
@@ -779,9 +779,9 @@ int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
 drop_unlock:
 	spin_unlock(&x->lock);
 drop:
+	xfrm_rcv_cb(skb, family, xfrm_proto, -1);
 	if (async)
 		dev_put(dev);
-	xfrm_rcv_cb(skb, family, xfrm_proto, -1);
 	kfree_skb(skb);
 	return 0;
 }
-- 
2.54.0 (Apple Git-157)


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH net v5 2/3] xfrm: set the address family before the early invalid-state drop
  2026-10-19 15:32 [PATCH net v5 0/3] xfrm: fix vti tunnel device use-after-free across async crypto resumption Qihang
  2026-10-19 15:32 ` [PATCH net v5 1/3] xfrm: run the receive callback before dropping the skb->dev reference Qihang
@ 2026-10-19 15:32 ` Qihang
  2026-10-19 15:32 ` [PATCH net v5 3/3] xfrm: hold the tunnel device reference across the input path Qihang
  2026-10-19 15:55 ` [PATCH net v5 0/3] xfrm: fix vti tunnel device use-after-free across async crypto resumption netdev-bot+sinfo
  3 siblings, 0 replies; 8+ messages in thread
From: Qihang @ 2026-10-19 15:32 UTC (permalink / raw)
  To: netdev
  Cc: steffen.klassert, chopps, herbert, davem, edumazet, kuba, pabeni,
	horms, dsahern, idosch, linux-kernel

The early drop in xfrm_input() for a state that is no longer valid runs
while family is still AF_UNSPEC, so xfrm_rcv_cb() cannot find the
afinfo and the receive callback is never invoked for such a packet.
Set the family from the state before the check so that the drop path
dispatches the callback like every other drop does.

This is also a prerequisite for releasing the tunnel device reference
on that drop path in the next patch: without a family, the release
cannot tell an ip_tunnel from an ip6_tnl.

Fixes: 4ce3dbe397d7 ("xfrm: Fix xfrm_input() to verify state is valid when (encap_type < 0)")
Cc: stable@vger.kernel.org
Signed-off-by: Qihang <q.h.hack.winter@gmail.com>
---
 net/xfrm/xfrm_input.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/net/xfrm/xfrm_input.c b/net/xfrm/xfrm_input.c
index 94de4f698c33..517369ba9efe 100644
--- a/net/xfrm/xfrm_input.c
+++ b/net/xfrm/xfrm_input.c
@@ -487,6 +487,7 @@ int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
 				      encap_type == UDP_ENCAP_ESPINUDP))) {
 		x = xfrm_input_state(skb);
 		xfrm_proto = x->type ? x->type->proto : nexthdr;
+		family = x->props.family;
 
 		if (unlikely(x->km.state != XFRM_STATE_VALID)) {
 			if (x->km.state == XFRM_STATE_ACQ)
@@ -500,8 +501,6 @@ int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
 			goto drop;
 		}
 
-		family = x->props.family;
-
 		/* An encap_type of -2 indicates reconstructed inner packet */
 		if (encap_type == -2)
 			goto resume_decapped;
-- 
2.54.0 (Apple Git-157)


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH net v5 3/3] xfrm: hold the tunnel device reference across the input path
  2026-10-19 15:32 [PATCH net v5 0/3] xfrm: fix vti tunnel device use-after-free across async crypto resumption Qihang
  2026-10-19 15:32 ` [PATCH net v5 1/3] xfrm: run the receive callback before dropping the skb->dev reference Qihang
  2026-10-19 15:32 ` [PATCH net v5 2/3] xfrm: set the address family before the early invalid-state drop Qihang
@ 2026-10-19 15:32 ` Qihang
  2026-10-22  3:34   ` netdev-bot+sashiko
  2026-10-19 15:55 ` [PATCH net v5 0/3] xfrm: fix vti tunnel device use-after-free across async crypto resumption netdev-bot+sinfo
  3 siblings, 1 reply; 8+ messages in thread
From: Qihang @ 2026-10-19 15:32 UTC (permalink / raw)
  To: netdev
  Cc: steffen.klassert, chopps, herbert, davem, edumazet, kuba, pabeni,
	horms, dsahern, idosch, linux-kernel

vti_input() and vti6_input_proto() cache the tunnel pointer in the skb
control buffer without taking a reference.  With async crypto the
receive callback runs from the crypto completion, after a possible
device teardown, and dereferences the freed tunnel device:

    BUG: KASAN: slab-use-after-free in vti_rcv_cb

Take a reference on the tunnel device when the pointer is cached.  The
tunnel is the device's private area, so the reference keeps the tunnel
structure alive as well.  xfrm6_rcv_spi() takes the same reference for
the tunnel it is given; it is a separate entry point, so vti6 does not
hold the reference twice.

A successful vti/vti6 callback leaves the cached tunnel in place for
xfrm_input() to release after delivery.  A callback that fails only
updates the error counters; the common drop path releases the reference
after the skb has been freed.  The helper takes the device out of the
skb before gro_cells_receive() and transport_finish(), which may already
consume the skb.  The device reference is therefore always dropped after
the skb is gone.

IPTFS consumes the outer skb itself and frees it directly, so it must
release the reference as well.  The IPTFS state carries the address
family the tunnel was resolved for, and the caller passes it in.  Add
xfrm_input_drop() and xfrm_input_consume() for the outer-skb free paths
in __input_process_payload(), iptfs_input_ordered(), iptfs_input(),
iptfs_drop_timer() and iptfs_destroy_state().  Only the outer skb
carries the cached tunnel; the inner skbs built by iptfs_alloc_skb()
have a zeroed control buffer, so they do not release it.  The drop paths
keep their skb drop reason and the consumed skb keeps consume_skb()
semantics.  An inner packet that fails validation reaches the drop path
before the queued inner packets are delivered; drop those as well so the
outer skb's reference is not leaked.

The reference is released right after gro_cells_receive(); the NAPI
that processes the queued skb later still uses skb->dev without a
reference.  That window is pre-existing and is not changed here.

This depends on the previous two patches: the receive callbacks must
run before the async dev_put(), and the early drop must know the
address family to release the reference.

Fixes: df3893c176e9 ("vti: Update the ipv4 side to use it's own receive hook.")
Fixes: fa9ad96d4905 ("vti6: Update the ipv6 side to use its own receive hook.")
Cc: stable@vger.kernel.org
Signed-off-by: Qihang <q.h.hack.winter@gmail.com>
---
v5:
- __input_process_payload(): drop the inner packets queued on the
  delivery list when an inner packet fails validation and jumps to the
  drop label, so the outer skb's tunnel device reference is released
  (found by the Sashiko review).

 net/ipv4/ip_vti.c      |  3 ++
 net/ipv6/ip6_vti.c     |  2 ++
 net/ipv6/xfrm6_input.c |  3 ++
 net/xfrm/xfrm_inout.h  |  4 +++
 net/xfrm/xfrm_input.c  | 63 +++++++++++++++++++++++++++++++++++++++++-
 net/xfrm/xfrm_iptfs.c  | 42 ++++++++++++++++++----------
 6 files changed, 102 insertions(+), 15 deletions(-)

diff --git a/net/ipv4/ip_vti.c b/net/ipv4/ip_vti.c
index 3b80929994a0..0277812852e7 100644
--- a/net/ipv4/ip_vti.c
+++ b/net/ipv4/ip_vti.c
@@ -63,6 +63,9 @@ static int vti_input(struct sk_buff *skb, int nexthdr, __be32 spi,
 
 		XFRM_TUNNEL_SKB_CB(skb)->tunnel.ip4 = tunnel;
 
+		/* Keep the tunnel alive until xfrm_input() is done with the skb. */
+		dev_hold(tunnel->dev);
+
 		if (update_skb_dev)
 			skb->dev = tunnel->dev;
 
diff --git a/net/ipv6/ip6_vti.c b/net/ipv6/ip6_vti.c
index ab94b3a4ba9c..6715bf10bc85 100644
--- a/net/ipv6/ip6_vti.c
+++ b/net/ipv6/ip6_vti.c
@@ -319,6 +319,8 @@ static int vti6_input_proto(struct sk_buff *skb, int nexthdr, __be32 spi,
 			goto discard;
 		}
 
+		/* Keep the tunnel alive until xfrm_input() is done with the skb. */
+		dev_hold(t->dev);
 		rcu_read_unlock();
 
 		XFRM_TUNNEL_SKB_CB(skb)->tunnel.ip6 = t;
diff --git a/net/ipv6/xfrm6_input.c b/net/ipv6/xfrm6_input.c
index 07edef258984..d27bc24f1b16 100644
--- a/net/ipv6/xfrm6_input.c
+++ b/net/ipv6/xfrm6_input.c
@@ -15,6 +15,7 @@
 #include <linux/netfilter.h>
 #include <linux/netfilter_ipv6.h>
 #include <net/ipv6.h>
+#include <net/ip6_tunnel.h>
 #include <net/xfrm.h>
 #include <net/protocol.h>
 #include <net/gro.h>
@@ -23,6 +24,8 @@ int xfrm6_rcv_spi(struct sk_buff *skb, int nexthdr, __be32 spi,
 		  struct ip6_tnl *t)
 {
 	XFRM_TUNNEL_SKB_CB(skb)->tunnel.ip6 = t;
+	if (t)
+		dev_hold(t->dev);
 	XFRM_SPI_SKB_CB(skb)->family = AF_INET6;
 	XFRM_SPI_SKB_CB(skb)->daddroff = offsetof(struct ipv6hdr, daddr);
 	return xfrm_input(skb, nexthdr, spi, 0);
diff --git a/net/xfrm/xfrm_inout.h b/net/xfrm/xfrm_inout.h
index efc5e6b2e87b..8bd05f62ad56 100644
--- a/net/xfrm/xfrm_inout.h
+++ b/net/xfrm/xfrm_inout.h
@@ -6,6 +6,10 @@
 #ifndef XFRM_INOUT_H
 #define XFRM_INOUT_H 1
 
+void xfrm_input_drop(struct sk_buff *skb, unsigned int family,
+		     enum skb_drop_reason reason);
+void xfrm_input_consume(struct sk_buff *skb, unsigned int family);
+
 static inline void xfrm4_extract_header(struct sk_buff *skb)
 {
 	const struct iphdr *iph = ip_hdr(skb);
diff --git a/net/xfrm/xfrm_input.c b/net/xfrm/xfrm_input.c
index 517369ba9efe..369817c870ce 100644
--- a/net/xfrm/xfrm_input.c
+++ b/net/xfrm/xfrm_input.c
@@ -120,6 +120,56 @@ static int xfrm_rcv_cb(struct sk_buff *skb, unsigned int family, u8 protocol,
 	return ret;
 }
 
+/*
+ * The cached tunnel holds one reference on its net_device.
+ * The address family determines the tunnel type.
+ */
+static struct net_device *xfrm_input_take_tunnel_dev(struct sk_buff *skb,
+						     unsigned int family)
+{
+	void *slot = (void *)XFRM_TUNNEL_SKB_CB(skb)->tunnel.ip4;
+	struct net_device *dev;
+
+	if (!slot)
+		return NULL;
+
+	switch (family) {
+	case AF_INET:
+		dev = ((struct ip_tunnel *)slot)->dev;
+		break;
+	case AF_INET6:
+		dev = ((struct ip6_tnl *)slot)->dev;
+		break;
+	default:
+		WARN_ON_ONCE(1);
+		return NULL;
+	}
+
+	XFRM_TUNNEL_SKB_CB(skb)->tunnel.ip4 = NULL;
+	return dev;
+}
+
+void xfrm_input_drop(struct sk_buff *skb, unsigned int family,
+		     enum skb_drop_reason reason)
+{
+	struct net_device *dev = xfrm_input_take_tunnel_dev(skb, family);
+
+	kfree_skb_reason(skb, reason);
+	if (dev)
+		dev_put(dev);
+}
+EXPORT_SYMBOL(xfrm_input_drop);
+
+void xfrm_input_consume(struct sk_buff *skb, unsigned int family)
+{
+	struct net_device *dev = xfrm_input_take_tunnel_dev(skb, family);
+
+	consume_skb(skb);
+	if (dev)
+		dev_put(dev);
+}
+EXPORT_SYMBOL(xfrm_input_consume);
+
 struct sec_path *secpath_set(struct sk_buff *skb)
 {
 	struct sec_path *sp, *tmp = skb_ext_find(skb, SKB_EXT_SEC_PATH);
@@ -468,6 +518,7 @@ int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
 	const struct xfrm_state_afinfo *afinfo;
 	struct net *net = dev_net(skb->dev);
 	struct net_device *dev = skb->dev;
+	struct net_device *tdev;
 	int err;
 	__be32 seq;
 	__be32 seq_hi;
@@ -748,7 +799,11 @@ int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
 			skb_dst_drop(skb);
 		if (async)
 			dev_put(dev);
+		/* gro_cells_receive() may free the skb. */
+		tdev = xfrm_input_take_tunnel_dev(skb, family);
 		gro_cells_receive(&gro_cells, skb);
+		if (tdev)
+			dev_put(tdev);
 		rcu_read_unlock();
 		return 0;
 	} else {
@@ -756,6 +811,7 @@ int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
 		if (xo)
 			xfrm_gro = xo->flags & XFRM_GRO;
 
+		tdev = xfrm_input_take_tunnel_dev(skb, family);
 		err = -EAFNOSUPPORT;
 		afinfo = xfrm_state_afinfo_get_rcu(family);
 		if (likely(afinfo))
@@ -769,6 +825,8 @@ int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
 			gro_cells_receive(&gro_cells, skb);
 		}
 
+		if (tdev)
+			dev_put(tdev);
 		if (async)
 			dev_put(dev);
 		rcu_read_unlock();
@@ -779,9 +837,12 @@ int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
 	spin_unlock(&x->lock);
 drop:
 	xfrm_rcv_cb(skb, family, xfrm_proto, -1);
+	tdev = xfrm_input_take_tunnel_dev(skb, family);
+	kfree_skb(skb);
+	if (tdev)
+		dev_put(tdev);
 	if (async)
 		dev_put(dev);
-	kfree_skb(skb);
 	return 0;
 }
 EXPORT_SYMBOL(xfrm_input);
diff --git a/net/xfrm/xfrm_iptfs.c b/net/xfrm/xfrm_iptfs.c
index 6920940a35b4..2b202c7570f1 100644
--- a/net/xfrm/xfrm_iptfs.c
+++ b/net/xfrm/xfrm_iptfs.c
@@ -709,8 +709,9 @@ static void __iptfs_reassem_done(struct xfrm_iptfs_data *xtfs, bool free)
 
 	/* We don't care if it works locking takes care of things */
 	hrtimer_try_to_cancel(&xtfs->drop_timer);
-	if (free)
-		kfree_skb(xtfs->ra_newskb);
+	if (free && xtfs->ra_newskb)
+		xfrm_input_drop(xtfs->ra_newskb, xtfs->x->props.family,
+				SKB_DROP_REASON_NOT_SPECIFIED);
 	xtfs->ra_newskb = NULL;
 }
 
@@ -1204,15 +1205,23 @@ static bool __input_process_payload(struct xfrm_state *x, u32 data,
 	skb_abort_seq_read(skbseq);
 
 	if (defer) {
-		consume_skb(defer);
+		xfrm_input_consume(defer, x->props.family);
 	} else if (!first_skb) {
-		/* skb is the original passed in skb, but we didn't get far
-		 * enough to process it as the first_skb, if we had it would
-		 * either be save in ra_newskb, trimmed and sent on as an skb or
-		 * placed in defer to be freed.
-		 */
-		kfree_skb(skb);
+		/* The original skb was not consumed as the first inner packet. */
+		xfrm_input_drop(skb, x->props.family,
+				SKB_DROP_REASON_NOT_SPECIFIED);
+	}
+
+	/*
+	 * A validation failure can bypass delivery of @sublist.  Drop the
+	 * queued skbs so the outer skb's tunnel reference is released.
+	 */
+	list_for_each_entry_safe(skb, next, sublist, list) {
+		skb_list_del_init(skb);
+		xfrm_input_drop(skb, x->props.family,
+				SKB_DROP_REASON_NOT_SPECIFIED);
 	}
+
 	return true;
 }
 
@@ -1298,7 +1307,8 @@ static void iptfs_input_ordered(struct xfrm_state *x, struct sk_buff *skb)
 	if (!consumed) {
 		skb = skbseq.root_skb;
 		skb_abort_seq_read(&skbseq);
-		kfree_skb(skb);
+		xfrm_input_drop(skb, x->props.family,
+				SKB_DROP_REASON_NOT_SPECIFIED);
 	}
 }
 
@@ -1700,7 +1710,8 @@ static enum hrtimer_restart iptfs_drop_timer(struct hrtimer *me)
 	spin_unlock(&xtfs->drop_lock);
 
 	if (skb)
-		kfree_skb_reason(skb, SKB_DROP_REASON_FRAG_REASM_TIMEOUT);
+		xfrm_input_drop(skb, x->props.family,
+				SKB_DROP_REASON_FRAG_REASM_TIMEOUT);
 
 	if (count) {
 		list_for_each_entry_safe(skb, next, &list, list) {
@@ -1751,7 +1762,8 @@ static int iptfs_input(struct xfrm_state *x, struct sk_buff *skb)
 
 	list_for_each_entry_safe(skb, next, &freelist, list) {
 		skb_list_del_init(skb);
-		kfree_skb(skb);
+		xfrm_input_drop(skb, x->props.family,
+				SKB_DROP_REASON_NOT_SPECIFIED);
 	}
 done:
 	/* We always have dealt with the input SKB, either we are re-using it,
@@ -2752,11 +2764,13 @@ static void iptfs_destroy_state(struct xfrm_state *x)
 	hrtimer_cancel(&xtfs->drop_timer);
 
 	if (xtfs->ra_newskb)
-		kfree_skb(xtfs->ra_newskb);
+		xfrm_input_drop(xtfs->ra_newskb, xtfs->x->props.family,
+				SKB_DROP_REASON_NOT_SPECIFIED);
 
 	for (s = xtfs->w_saved, se = s + xtfs->w_savedlen; s < se; s++) {
 		if (s->skb)
-			kfree_skb(s->skb);
+			xfrm_input_drop(s->skb, x->props.family,
+					SKB_DROP_REASON_NOT_SPECIFIED);
 	}
 
 	kfree_sensitive(xtfs->w_saved);
-- 
2.54.0 (Apple Git-157)


^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH net v5 0/3] xfrm: fix vti tunnel device use-after-free across async crypto resumption
  2026-10-19 15:32 [PATCH net v5 0/3] xfrm: fix vti tunnel device use-after-free across async crypto resumption Qihang
                   ` (2 preceding siblings ...)
  2026-10-19 15:32 ` [PATCH net v5 3/3] xfrm: hold the tunnel device reference across the input path Qihang
@ 2026-10-19 15:55 ` netdev-bot+sinfo
  2026-10-20 20:20   ` Jakub Kicinski
  3 siblings, 1 reply; 8+ messages in thread
From: netdev-bot+sinfo @ 2026-10-19 15:55 UTC (permalink / raw)
  To: Qihang
  Cc: netdev, steffen.klassert, chopps, herbert, davem, edumazet, kuba,
	pabeni, horms, dsahern, idosch, linux-kernel

Hi!

This is an automated message. This series looks like a fix, but its
commit messages seem to be missing some information:

 - How the issue was discovered, e.g. hit in production, hit during
   development, syzbot report, manual code inspection, LLM or static
   analysis tool scan.

Please do not repost the series just to address the above. Instead,
reply to this email with the missing information, so that reviewers
can take it into account. If the series needs another revision for
other reasons, please include the information in the commit messages
then.

The evaluation is done by an LLM so it may be wrong, if you think
that is the case please reply and explain.

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH net v5 0/3] xfrm: fix vti tunnel device use-after-free across async crypto resumption
  2026-10-19 15:55 ` [PATCH net v5 0/3] xfrm: fix vti tunnel device use-after-free across async crypto resumption netdev-bot+sinfo
@ 2026-10-20 20:20   ` Jakub Kicinski
  0 siblings, 0 replies; 8+ messages in thread
From: Jakub Kicinski @ 2026-10-20 20:20 UTC (permalink / raw)
  To: Qihang
  Cc: netdev-bot+sinfo, netdev, steffen.klassert, chopps, herbert,
	davem, edumazet, pabeni, horms, dsahern, idosch, linux-kernel

On Mon, 19 Oct 2026 15:55:41 +0000 netdev-bot+sinfo@kernel.org wrote:
>  - How the issue was discovered, e.g. hit in production, hit during
>    development, syzbot report, manual code inspection, LLM or static
>    analysis tool scan.

Please include the answer to this question in the cover letter.

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH net v5 1/3] xfrm: run the receive callback before dropping the skb->dev reference
  2026-10-19 15:32 ` [PATCH net v5 1/3] xfrm: run the receive callback before dropping the skb->dev reference Qihang
@ 2026-10-22  3:34   ` netdev-bot+sashiko
  0 siblings, 0 replies; 8+ messages in thread
From: netdev-bot+sashiko @ 2026-10-22  3:34 UTC (permalink / raw)
  To: q.h.hack.winter
  Cc: netdev, steffen.klassert, chopps, herbert, davem, edumazet, kuba,
	pabeni, horms, dsahern, idosch, linux-kernel

Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.

Critical: 0 · High: 1 · Medium: 0 · Low: 0

If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).

- [High] The tunnel-mode (decaps) success exit of xfrm_input() in
  net/xfrm/xfrm_input.c still runs `if (async) dev_put(dev);` before…

--

xfrm: run the receive callback before dropping the skb->dev reference

On the drop path of xfrm_input(), this commit moves xfrm_rcv_cb() so it
runs before the async skb->dev reference is released. It also changes
the early drop for a state that is no longer valid: instead of calling
dev_put() itself, that path now leaves the reference to the drop label.

> diff --git a/net/xfrm/xfrm_input.c b/net/xfrm/xfrm_input.c
> index 5ed87d51392a..94de4f698c33 100644
> --- a/net/xfrm/xfrm_input.c
> +++ b/net/xfrm/xfrm_input.c

[ ... ]

> @@ -779,9 +779,9 @@ int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
>  drop_unlock:
>  	spin_unlock(&x->lock);
>  drop:
> +	xfrm_rcv_cb(skb, family, xfrm_proto, -1);
>  	if (async)
>  		dev_put(dev);
> -	xfrm_rcv_cb(skb, family, xfrm_proto, -1);
>  	kfree_skb(skb);
>  	return 0;
>  }

[Severity: High]
The commit message says the new order matches what "the success path
does". Is that true of the tunnel mode (decaps) exit of xfrm_input()?
That exit still drops the async reference before it passes the skb to
gro_cells_receive():

net/xfrm/xfrm_input.c:xfrm_input() {
    ...
	if (decaps) {
		...
		if (async)
			dev_put(dev);
		gro_cells_receive(&gro_cells, skb);
		rcu_read_unlock();
		return 0;
    ...
}

gro_cells_receive() then dereferences skb->dev:

net/core/gro_cells.c:gro_cells_receive() {
	struct net_device *dev = skb->dev;
	...
	if (unlikely(!(dev->flags & IFF_UP)))
		goto drop;
	...
drop:
	dev_core_stats_rx_dropped_inc(dev);
	...
}

In plain policy-based ESP/AH tunnel mode, no xfrmi or vti callback
replaces skb->dev. So skb->dev is the same lower device whose reference
was just released.

Suppose an async crypto completion runs while the lower device is being
unregistered:

esp_input_done()
  xfrm_input_resume()
    xfrm_input(skb, nexthdr, 0, -1)

Can the device refcount reach 1 after this dev_put()?

netdev_wait_allrefs_any() in net/core/dev.c returns as soon as
netdev_refcnt_read(dev) == 1. netdev_run_todo() then frees the device
without waiting for a grace period, so the rcu_read_lock() held here
does not seem to keep the device alive.

If the device is going away, IFF_UP is already clear. In that case
gro_cells_receive() would take its drop branch and write the core stats
of a net_device that may already be freed.

This ordering came in with 1c428b038400, the commit named in the Fixes:
tag, and 8045c0df98d4 kept it. It also seems to survive the later commit
"xfrm: hold the tunnel device reference across the input path", which
only adds tdev handling around gro_cells_receive().

Since dev is already saved in a local variable, could the decaps branch
also move the if (async) dev_put(dev) below gro_cells_receive()?

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261019153247.89957-1-q.h.hack.winter%40gmail.com

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH net v5 3/3] xfrm: hold the tunnel device reference across the input path
  2026-10-19 15:32 ` [PATCH net v5 3/3] xfrm: hold the tunnel device reference across the input path Qihang
@ 2026-10-22  3:34   ` netdev-bot+sashiko
  0 siblings, 0 replies; 8+ messages in thread
From: netdev-bot+sashiko @ 2026-10-22  3:34 UTC (permalink / raw)
  To: q.h.hack.winter
  Cc: netdev, steffen.klassert, chopps, herbert, davem, edumazet, kuba,
	pabeni, horms, dsahern, idosch, linux-kernel

Thank you for your contribution! Sashiko AI review found 2 potential
issue(s) to consider.

Critical: 0 · High: 0 (2) · Medium: 1 · Low: 1

If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).

- [Medium] After this patch, every outer ESP skb received through
  vti/vti6 holds a reference on the tunnel net_device (dev_hold() in…
- [Low] The new loop at the end of __input_process_payload()
  (net/xfrm/xfrm_iptfs.c) does more than release the tunnel reference…

Pre-existing issues:
- [High] This problem wasn't introduced by this patch, but the patch
  leaves it in place and its message overstates its coverage.
- [High] This problem wasn't introduced by this patch, but IPTFS-parked
  skbs keep a skb->dev pointer with no reference on it.

--

xfrm: hold the tunnel device reference across the input path

This patch takes a reference on the vti/vti6 tunnel net_device when the
tunnel pointer is cached in the skb control buffer. xfrm_input() then
releases it after delivery or drop. New xfrm_input_drop() and
xfrm_input_consume() helpers are added, and the IPTFS outer skb free paths
are converted to use them so the reference is released there too.

> diff --git a/net/xfrm/xfrm_input.c b/net/xfrm/xfrm_input.c
> index 517369ba9efe..369817c870ce 100644
> --- a/net/xfrm/xfrm_input.c
> +++ b/net/xfrm/xfrm_input.c

[ ... ]

> @@ -748,7 +799,11 @@ int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
>  			skb_dst_drop(skb);
>  		if (async)
>  			dev_put(dev);
> +		/* gro_cells_receive() may free the skb. */
> +		tdev = xfrm_input_take_tunnel_dev(skb, family);
>  		gro_cells_receive(&gro_cells, skb);
> +		if (tdev)
> +			dev_put(tdev);
>  		rcu_read_unlock();
>  		return 0;
>  	} else {
> @@ -756,6 +811,7 @@ int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
>  		if (xo)
>  			xfrm_gro = xo->flags & XFRM_GRO;
>  
> +		tdev = xfrm_input_take_tunnel_dev(skb, family);
>  		err = -EAFNOSUPPORT;
>  		afinfo = xfrm_state_afinfo_get_rcu(family);
>  		if (likely(afinfo))
> @@ -769,6 +825,8 @@ int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
>  			gro_cells_receive(&gro_cells, skb);
>  		}
>  
> +		if (tdev)
> +			dev_put(tdev);

[Severity: High]
This is a pre-existing issue and was not introduced by this patch. In both
paths in xfrm_input(), the tunnel reference is dropped right after the skb
goes onto a deferred queue. At that point skb->dev still points at the vti
device, because vti_rcv_cb() and vti6_rcv_cb() set it:

    skb->dev = dev;

The gro_cells used here is the single static instance bound to
xfrm_napi_dev. Nothing drains it when a vti device is unregistered.
flush_all_backlogs() only covers the softnet_data backlogs, and the
gro_cells_destroy() call in ip_tunnel only covers the tunnel's own
gro_cells.

The transport path has the same shape:

  transport_finish()->NF_HOOK()->xfrm4_rcv_encap_finish()
    ->xfrm_trans_queue()

net/ipv4/xfrm4_input.c:xfrm4_rcv_encap_finish() {
    ...
	if (xfrm_trans_queue(skb, xfrm4_rcv_encap_finish2))
		goto drop;
    ...
}

That defers the skb to the per-CPU xfrm_trans tasklet, and dev_put(tdev)
follows straight away.

Suppose the softirq or tasklet holding the skb is delayed, for example to
ksoftirqd or under PREEMPT_RT. Could synchronize_net() and the
rcu_barrier() in netdev_run_todo() then complete? If so,
netdev_wait_allrefs_any() would see no extra reference and the vti
net_device could be freed. gro_cell_poll()->napi_gro_receive(), or the
tasklet's ip_route_input()/dst_input(), would then use the freed
skb->dev.

The commit message says:

  "The device reference is therefore always dropped after the skb is
  gone."

For these queued paths the skb has been queued, not freed. The message
mentions the gro_cells window but not the xfrm_trans_queue() one. Could
it be updated to say so?

>  		if (async)
>  			dev_put(dev);
>  		rcu_read_unlock();

[ ... ]

> diff --git a/net/xfrm/xfrm_iptfs.c b/net/xfrm/xfrm_iptfs.c
> index 6920940a35b4..2b202c7570f1 100644
> --- a/net/xfrm/xfrm_iptfs.c
> +++ b/net/xfrm/xfrm_iptfs.c
> @@ -709,8 +709,9 @@ static void __iptfs_reassem_done(struct xfrm_iptfs_data *xtfs, bool free)
>  
>  	/* We don't care if it works locking takes care of things */
>  	hrtimer_try_to_cancel(&xtfs->drop_timer);
> -	if (free)
> -		kfree_skb(xtfs->ra_newskb);
> +	if (free && xtfs->ra_newskb)
> +		xfrm_input_drop(xtfs->ra_newskb, xtfs->x->props.family,
> +				SKB_DROP_REASON_NOT_SPECIFIED);
>  	xtfs->ra_newskb = NULL;
>  }

[Severity: Medium]
With this patch, the dev_hold() in vti_input(), vti6_input_proto() and
xfrm6_rcv_spi() means every outer ESP skb received through vti/vti6 pins
the tunnel net_device. The pin lasts until the skb is freed through
xfrm_input_drop() or xfrm_input_consume().

IPTFS can keep outer skbs for a long time:

  - in xtfs->ra_newskb, when the reused first_skb is a partial datagram
  - in the reorder window xtfs->w_saved[] (reorder_win_size defaults
    to 3)

While they are parked, unregister_netdevice() of the vti device waits in
netdev_wait_allrefs_any(). The wait is at least drop_time_ns: 1s by
default, and up to about 4295s through XFRMA_IPTFS_DROP_TIME.

Can that wait become unbounded? ra_newskb and w_saved share drop_timer,
and __iptfs_reassem_done() cancels it unconditionally, as shown above.
Consider this sequence:

  seq N starts reassembly, drop_timer is armed
  seq N+3 is saved in w_saved[1]; iptfs_set_window_drop_times() does
    not re-arm because the timer is already queued
  seq N+1 arrives in order; __reorder_this() shifts N+3 into
    w_saved[0] with __vec_shift(), and N+1 completes the reassembly
  __iptfs_reassem_done() cancels drop_timer

N+3 now sits in w_saved[0] with no timer. It is only released when more
traffic arrives on that SA.

Its sec_path holds a reference on the xfrm_state, so
xfrm_state_gc_destroy()->iptfs_destroy_state() never runs. The new
xfrm_input_drop() calls in iptfs_destroy_state() therefore can't release
it either.

If the vti device and its SAs are removed, or the netns is torn down,
would unregistration then hang waiting for the vti device to become
free?

The timer cancel behaviour predates this patch. Pinning the tunnel device
from these parked skbs is new, and the commit message doesn't mention it.

[ ... ]

> @@ -1204,15 +1205,23 @@ static bool __input_process_payload(struct xfrm_state *x, u32 data,
>  	skb_abort_seq_read(skbseq);
>  
>  	if (defer) {
> -		consume_skb(defer);
> +		xfrm_input_consume(defer, x->props.family);
>  	} else if (!first_skb) {
> -		/* skb is the original passed in skb, but we didn't get far
> -		 * enough to process it as the first_skb, if we had it would
> -		 * either be save in ra_newskb, trimmed and sent on as an skb or
> -		 * placed in defer to be freed.
> -		 */
> -		kfree_skb(skb);
> +		/* The original skb was not consumed as the first inner packet. */
> +		xfrm_input_drop(skb, x->props.family,
> +				SKB_DROP_REASON_NOT_SPECIFIED);
> +	}
> +
> +	/*
> +	 * A validation failure can bypass delivery of @sublist.  Drop the
> +	 * queued skbs so the outer skb's tunnel reference is released.
> +	 */
> +	list_for_each_entry_safe(skb, next, sublist, list) {
> +		skb_list_del_init(skb);
> +		xfrm_input_drop(skb, x->props.family,
> +				SKB_DROP_REASON_NOT_SPECIFIED);
>  	}
> +
>  	return true;
>  }

[Severity: Low]
The commit message describes this loop only as "drop those as well so
the outer skb's reference is not leaked". Doesn't it also fix an older
skb leak in __input_process_payload()?

Before this patch, several validation failures jump to the done label
and skip the "Send the packets!" loop:

  - skb_copy_seq_read() failures
  - an IPv4 header with a bad ihl/tot_len
  - a version nibble that is not 0, 4 or 6

Anything already queued on @sublist was never freed. That could be
earlier inner skbs, the reused first_skb, or a datagram that
iptfs_reassem_cont() had just completed. The done label only freed the
root skb when !first_skb, and iptfs_input_ordered() never looks at
@sublist again.

Each leaked skb also kept its sec_path, which holds a reference on the
xfrm_state through sp->xvec.

That leak dates back to 6c82d24336718 ("xfrm: iptfs: add basic receive
packet (tunnel egress) handling"). Should the commit message mention
it, and should a Fixes: tag for that commit be added? The current Fixes:
tags only point at the vti commits.

[ ... ]

> @@ -1700,7 +1710,8 @@ static enum hrtimer_restart iptfs_drop_timer(struct hrtimer *me)
>  	spin_unlock(&xtfs->drop_lock);
>  
>  	if (skb)
> -		kfree_skb_reason(skb, SKB_DROP_REASON_FRAG_REASM_TIMEOUT);
> +		xfrm_input_drop(skb, x->props.family,
> +				SKB_DROP_REASON_FRAG_REASM_TIMEOUT);
>  
>  	if (count) {
>  		list_for_each_entry_safe(skb, next, &list, list) {

[Severity: High]
This is a pre-existing issue and was not introduced by this patch. The
skbs IPTFS parks keep a skb->dev pointer with no reference held on it.

The device here is the physical ingress device, not the vti device.
IPTFS is ESP-only, and vti_rcv_proto() calls vti_input() with
update_skb_dev=false. The tunnel reference added in this patch therefore
doesn't cover it.

xfrm_input() only holds the ingress device across x->type->input. When
xfrm_inner_mode_input() returns -EINPROGRESS for IPTFS, it does:

	err = xfrm_inner_mode_input(x, skb);
	if (err == -EINPROGRESS) {
		if (async)
			dev_put(dev);
		return 0;

After that, two kinds of parked skb carry an unreferenced skb->dev:

  - xtfs->ra_newskb, which is either the reused outer skb or a fresh skb
    from iptfs_alloc_skb()
  - the outer skbs in xtfs->w_saved[]

iptfs_alloc_skb() copies the device without a hold:

net/xfrm/xfrm_iptfs.c:iptfs_alloc_skb() {
    ...
		/* xfrm_input resume needs dev and xfrm ext from tunnel pkt */
		skb->dev = tpl->dev;
		__skb_ext_copy(skb, tpl);
    ...
}

Suppose the ingress device is unregistered within drop_time, for example
a veth during container netns teardown, or a removed NIC. Two paths can
still deliver a parked skb afterwards. One is a completion arriving on
another interface through iptfs_reassem_cont(). The other is this timer
replay:

  iptfs_drop_timer()->__reorder_drop()->iptfs_input_ordered()
    ->__input_process_payload()->xfrm_input(skb, 0, 0, -2)

Both reach xfrm_input(), which starts with:

	struct net *net = dev_net(skb->dev);

gro_cells_receive() later reads dev->flags. Can either of these
dereference the freed net_device?

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261019153247.89957-1-q.h.hack.winter%40gmail.com

^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-10-22  3:34 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-19 15:32 [PATCH net v5 0/3] xfrm: fix vti tunnel device use-after-free across async crypto resumption Qihang
2026-10-19 15:32 ` [PATCH net v5 1/3] xfrm: run the receive callback before dropping the skb->dev reference Qihang
2026-10-22  3:34   ` netdev-bot+sashiko
2026-10-19 15:32 ` [PATCH net v5 2/3] xfrm: set the address family before the early invalid-state drop Qihang
2026-10-19 15:32 ` [PATCH net v5 3/3] xfrm: hold the tunnel device reference across the input path Qihang
2026-10-22  3:34   ` netdev-bot+sashiko
2026-10-19 15:55 ` [PATCH net v5 0/3] xfrm: fix vti tunnel device use-after-free across async crypto resumption netdev-bot+sinfo
2026-10-20 20:20   ` Jakub Kicinski

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®