[PATCH net v2 3/3] xfrm: transfer the skb->dev reference to the input path

From: Qihang

Date: Tue Oct 13 2026 - 03:48:53 EST


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

Taking a reference while the pointer is cached and dropping it in the
callback does not fix that: on the success path the callback drops what
may be the last reference before xfrm_input() hands the skb to
gro_cells_receive(), and taking a new reference on skb->dev at that
point is already too late, the device may have been freed in between:

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

Make the reference travel with the skb instead: the receive callbacks
install the device in skb->dev and leave their reference with it, and
xfrm_input() releases it once the packet has been delivered. vti,
vti6 and xfrmi -- the only callbacks that replace skb->dev -- take the
reference when installing the device and drop it on their error paths.

Tested on arm64 with KASAN and an async AEAD: no use-after-free on
either completion path, and RTM_DELLINK on a VTI device with pending
async crypto now waits for the completion.

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@xxxxxxxxxxxxxxx
Signed-off-by: Qihang <q.h.hack.winter@xxxxxxxxx>
---
net/ipv4/ip_vti.c | 6 ++++++
net/ipv6/ip6_vti.c | 7 +++++++
net/xfrm/xfrm_input.c | 13 +++++++++++++
net/xfrm/xfrm_interface_core.c | 9 ++++++++-
4 files changed, 34 insertions(+), 1 deletion(-)

diff --git a/net/ipv4/ip_vti.c b/net/ipv4/ip_vti.c
index 3b80929994a0..1964482064c1 100644
--- a/net/ipv4/ip_vti.c
+++ b/net/ipv4/ip_vti.c
@@ -63,6 +63,8 @@ static int vti_input(struct sk_buff *skb, int nexthdr, __be32 spi,

XFRM_TUNNEL_SKB_CB(skb)->tunnel.ip4 = tunnel;

+ dev_hold(tunnel->dev);
+
if (update_skb_dev)
skb->dev = tunnel->dev;

@@ -109,10 +111,14 @@ static int vti_rcv_cb(struct sk_buff *skb, int err)

dev = tunnel->dev;

+ /* Drop the reference taken in vti_input(); on success it is
+ * transferred with skb->dev and released by xfrm_input().
+ */
if (err) {
DEV_STATS_INC(dev, rx_errors);
DEV_STATS_INC(dev, rx_dropped);

+ dev_put(dev);
return 0;
}

diff --git a/net/ipv6/ip6_vti.c b/net/ipv6/ip6_vti.c
index ab94b3a4ba9c..a47acae4756c 100644
--- a/net/ipv6/ip6_vti.c
+++ b/net/ipv6/ip6_vti.c
@@ -322,6 +322,9 @@ static int vti6_input_proto(struct sk_buff *skb, int nexthdr, __be32 spi,
rcu_read_unlock();

XFRM_TUNNEL_SKB_CB(skb)->tunnel.ip6 = 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, encap_type);
@@ -355,10 +358,14 @@ static int vti6_rcv_cb(struct sk_buff *skb, int err)

dev = t->dev;

+ /* Drop the reference taken in vti6_input_proto(); on success it is
+ * transferred with skb->dev and released by xfrm_input().
+ */
if (err) {
DEV_STATS_INC(dev, rx_errors);
DEV_STATS_INC(dev, rx_dropped);

+ dev_put(dev);
return 0;
}

diff --git a/net/xfrm/xfrm_input.c b/net/xfrm/xfrm_input.c
index f293d5e124d4..bdd493f9d877 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)
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;
@@ -767,7 +768,13 @@ int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
skb_dst_drop(skb);
if (async)
dev_put(dev);
+ /* Release the reference a receive callback left with
+ * skb->dev, after the packet has been delivered.
+ */
+ tdev = skb->dev != dev ? skb->dev : NULL;
gro_cells_receive(&gro_cells, skb);
+ if (tdev)
+ dev_put(tdev);
rcu_read_unlock();
return 0;
} else {
@@ -775,6 +782,10 @@ int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
if (xo)
xfrm_gro = xo->flags & XFRM_GRO;

+ /* Release the reference a receive callback left with
+ * skb->dev, after the packet has been delivered.
+ */
+ tdev = skb->dev != dev ? skb->dev : NULL;
err = -EAFNOSUPPORT;
afinfo = xfrm_state_afinfo_get_rcu(family);
if (likely(afinfo))
@@ -788,6 +799,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();
diff --git a/net/xfrm/xfrm_interface_core.c b/net/xfrm/xfrm_interface_core.c
index 688306bf62c5..f4859be7ed18 100644
--- a/net/xfrm/xfrm_interface_core.c
+++ b/net/xfrm/xfrm_interface_core.c
@@ -375,11 +375,18 @@ static int xfrmi_rcv_cb(struct sk_buff *skb, int err)

link = skb->dev->ifindex;
dev = xi->dev;
- skb->dev = dev;
+ if (skb->dev != dev) {
+ /* Reference travels with skb->dev; released by xfrm_input()
+ * after delivery. Guard makes a re-invocation a no-op.
+ */
+ dev_hold(dev);
+ skb->dev = dev;
+ }

if (err) {
DEV_STATS_INC(dev, rx_errors);
DEV_STATS_INC(dev, rx_dropped);
+ dev_put(dev);

return 0;
}
--
2.54.0 (Apple Git-157)