Thread (28 messages) flat view 28 messages, 5 authors, 15d ago

Re: [PATCH net-next 7/9] sit: dynamically allocate struct ip_tunnel_parm_kern

From: netdev-bot+sashiko@kernel.org
Date: 2026-09-08 11:00:59

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

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

- [Medium] Intermediate-state use-after-free in net/ipv6/sit.c:
  ipip6_tunnel_update() publishes the freshly allocated parameter block…
- [Low] Missing publication barrier / unannotated lockless pointer: in
  this commit `sit_parms` is a plain (non-__rcu) pointer in struct…

--

sit: dynamically allocate struct ip_tunnel_parm_kern

This moves the SIT configuration parameters out of the embedded
ip_tunnel.parms into a separately allocated struct ip_tunnel_parm_kern
reachable through the new ip_tunnel.sit_parms pointer, in preparation for
converting them to RCU.  All readers and writers in net/ipv6/sit.c are
switched over to the new pointer.

A couple of questions about the intermediate state this commit leaves the
tree in.
quoted hunk ↗ jump to hunk
diff --git a/include/net/ip_tunnels.h b/include/net/ip_tunnels.h
index 7fff59bab53b6..f464c4480edaf 100644
--- a/include/net/ip_tunnels.h
+++ b/include/net/ip_tunnels.h
@@ -190,6 +190,7 @@ struct ip_tunnel {
 #endif
 	struct ip_tunnel_prl_entry __rcu *prl;	/* potential router list */
 	unsigned int		prl_count;	/* # of entries in PRL */
+	struct ip_tunnel_parm_kern *sit_parms;
      ^^^^

[Severity: Low]
Should this member already carry the __rcu annotation in this commit?  It
is read from softirq packet paths (ipip6_tunnel_lookup(), ipip6_rcv(),
sit_tunnel_rcv(), ipip6_tunnel_xmit(), sit_tunnel_xmit__()) without RTNL,
while ipip6_tunnel_update() replaces it with a plain store.  As a plain
pointer, sparse cannot flag the unprotected dereferences, the publish has
no release semantics, and the loads are plain data races for KCSAN.

The following patch in the series, "sit: convert configuration to RCU
protection", changes this to struct ip_tunnel_parm_kern __rcu *sit_parms
and converts publication to rcu_assign_pointer(), so the window is limited
to this single commit.  Would it be worth folding the annotation in here?
quoted hunk ↗ jump to hunk
 	unsigned int		ip_tnl_net_id;
 	struct gro_cells	gro_cells;
 	__u32			fwmark;
diff --git a/net/ipv6/sit.c b/net/ipv6/sit.c
index 35c6695909014..dc37c7109af53 100644
--- a/net/ipv6/sit.c
+++ b/net/ipv6/sit.c
@@ -108,24 +108,24 @@ static struct ip_tunnel *ipip6_tunnel_lookup(struct net *net,
 	int ifindex = dev ? dev->ifindex : 0;
 
 	for_each_ip_tunnel_rcu(t, sitn->tunnels_r_l[h0 ^ h1]) {
-		if (local == t->parms.iph.saddr &&
-		    remote == t->parms.iph.daddr &&
-		    (!dev || !t->parms.link || ifindex == t->parms.link ||
-		     sifindex == t->parms.link) &&
+		if (local == t->sit_parms->iph.saddr &&
+		    remote == t->sit_parms->iph.daddr &&
+		    (!dev || !t->sit_parms->link || ifindex == t->sit_parms->link ||
+		     sifindex == t->sit_parms->link) &&
        ^^^^

[Severity: Low]
Can this mix fields from two different parameter blocks?  t->sit_parms is
reloaded up to four times inside one match decision here, and
ipip6_tunnel_update() swaps the pointer concurrently, so saddr/daddr could
come from the old block and link from the new one.

The next patch caches a single rcu_dereference(t->sit_parms) in a local
parms variable per candidate, which removes this; could that be done here
instead?
 		    (t->dev->flags & IFF_UP))
 			return t;
 	}
[ ... ]
quoted hunk ↗ jump to hunk
@@ -938,9 +942,9 @@ static netdev_tx_t ipip6_tunnel_xmit(struct sk_buff *skb,
 				     struct net_device *dev)
 {
 	struct ip_tunnel *tunnel = netdev_priv(dev);
-	const struct iphdr  *tiph = &tunnel->parms.iph;
+	const struct iphdr  *tiph = &tunnel->sit_parms->iph;
 	const struct ipv6hdr *iph6 = ipv6_hdr(skb);
-	u8     tos = tunnel->parms.iph.tos;
+	u8     tos = tunnel->sit_parms->iph.tos;
        ^^^^

[Severity: Medium]
Can this read freed memory?  ipip6_tunnel_xmit() reaches the tunnel via
netdev_priv(dev), never through the sitn->tunnels[] buckets, and it keeps
the interior pointer tiph across the route lookup, the PMTU handling, the
headroom reallocation and iptunnel_xmit().

Meanwhile ipip6_tunnel_update() frees the block the pointer points into:

net/ipv6/sit.c:ipip6_tunnel_update() {
	...
	ipip6_tunnel_unlink(sitn, t);
	synchronize_net();
	t->sit_parms = new_p;
	...
	kfree(old_p);
	return 0;
}

The synchronize_net() runs before the new pointer is published and only
drains readers that came in through the hash buckets, so it does not cover
this path.  dev->lltx is set for sit, so nothing else serializes a
SIOCCHGTUNNEL or RTM_NEWLINK change against traffic on the same device.
The values read out of the freed block (tiph->saddr, tos, ttl, frag_off)
end up in the outer IPv4 header.

The immediately following patch, "sit: convert configuration to RCU
protection", wraps the transmit path in rcu_read_lock() and uses
kfree_rcu(old_p, rcu), so this hazard exists only between the two commits
(a concern for bisection and partial backports).
 	__be16 df = tiph->frag_off;
 	struct rtable *rt;		/* Route to the other host */
 	struct net_device *tdev;	/* Device to other host */
[ ... ]
quoted hunk ↗ jump to hunk
@@ -1083,7 +1087,7 @@ static netdev_tx_t sit_tunnel_xmit__(struct sk_buff *skb,
 				     struct net_device *dev, u8 ipproto)
 {
 	struct ip_tunnel *tunnel = netdev_priv(dev);
-	const struct iphdr  *tiph = &tunnel->parms.iph;
+	const struct iphdr  *tiph = &tunnel->sit_parms->iph;
        ^^^^

[Severity: Medium]
Same question here: this hands an interior pointer of the freeable
parameter block to ip_tunnel_xmit() with no reference and no RCU section
held, so a concurrent ipip6_tunnel_update() can kfree() it underneath.
 
 	if (iptunnel_handle_offloads(skb, SKB_GSO_IPXIP4))
 		goto tx_error;
[ ... ]
quoted hunk ↗ jump to hunk
@@ -1171,30 +1175,41 @@ static void ipip6_tunnel_bind_dev(struct net_device *dev)
 	dev->needed_headroom = ip_tunnel_limit_headroom(t_hlen + hlen);
 }
 
-static void ipip6_tunnel_update(struct ip_tunnel *t,
-				struct ip_tunnel_parm_kern *p,
-				__u32 fwmark)
+static int ipip6_tunnel_update(struct ip_tunnel *t,
+			       struct ip_tunnel_parm_kern *p,
+			       __u32 fwmark)
 {
 	struct net *net = t->net;
 	struct sit_net *sitn = net_generic(net, sit_net_id);
+	struct ip_tunnel_parm_kern *new_p, *old_p;
 
+	old_p = t->sit_parms;
+	new_p = kmalloc_obj(*new_p);
+	if (!new_p)
+		return -ENOMEM;
+	*new_p = *old_p;
+	new_p->iph.saddr = p->iph.saddr;
+	new_p->iph.daddr = p->iph.daddr;
+	new_p->iph.ttl = p->iph.ttl;
+	new_p->iph.tos = p->iph.tos;
+	new_p->iph.frag_off = p->iph.frag_off;
+	new_p->link = p->link;
 	ipip6_tunnel_unlink(sitn, t);
 	synchronize_net();
-	t->parms.iph.saddr = p->iph.saddr;
-	t->parms.iph.daddr = p->iph.daddr;
+	t->sit_parms = new_p;
        ^^^^

[Severity: Medium]
Is the ordering here intentional?  The grace period happens before the new
pointer is published, so nothing separates the store of new_p from the
kfree(old_p) below for readers that already loaded the old pointer.
+	t->parms.iph = new_p->iph;
+	WRITE_ONCE(t->parms.link, new_p->link);
 	__dev_addr_set(t->dev, &p->iph.saddr, 4);
 	memcpy(t->dev->broadcast, &p->iph.daddr, 4);
 	ipip6_tunnel_link(sitn, t);
-	t->parms.iph.ttl = p->iph.ttl;
-	t->parms.iph.tos = p->iph.tos;
-	t->parms.iph.frag_off = p->iph.frag_off;
-	if (t->parms.link != p->link || t->fwmark != fwmark) {
-		t->parms.link = p->link;
+	if (old_p->link != p->link || t->fwmark != fwmark) {
 		WRITE_ONCE(t->fwmark, fwmark);
 		ipip6_tunnel_bind_dev(t->dev);
 	}
 	dst_cache_reset(&t->dst_cache);
 	netdev_state_change(t->dev);
+	kfree(old_p);
        ^^^^

[Severity: Medium]
Would kfree_rcu() be needed here, given that ipip6_tunnel_xmit(),
sit_tunnel_xmit__() and ipip6_get_iflink() all reach sit_parms through
netdev_priv(dev) rather than through the hash buckets?

The next patch in the series adds a struct rcu_head to
ip_tunnel_parm_kern and turns this into kfree_rcu(old_p, rcu).
+	return 0;
 }
[ ... ]
quoted hunk ↗ jump to hunk
@@ -1448,7 +1465,7 @@ static int ipip6_get_iflink(const struct net_device *dev)
 {
 	struct ip_tunnel *tunnel = netdev_priv(dev);
 
-	return READ_ONCE(tunnel->parms.link);
+	return READ_ONCE(tunnel->sit_parms->link);
        ^^^^

[Severity: Medium]
Here the READ_ONCE() now covers only the link field, not the pointer load
itself.  Since this accessor can run without RTNL, can it dereference the
block that ipip6_tunnel_update() is about to kfree()?  The following patch
adds an rcu_read_lock() section and an rcu_dereference() here.
 }
 
 static const struct net_device_ops ipip6_netdev_ops = {
[ ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260907075846.2913645-1-edumazet%40google.com
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help