Thread (17 messages) flat view 17 messages, 4 authors, 2d ago

Re: [PATCH v2 net-next 5/8] vxlan: dynamically allocate struct vxlan_config

From: netdev-bot+sashiko@kernel.org
Date: 2026-09-08 18:03:55

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

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

- [High] vxlan_config_apply() publishes the newly kmemdup()'ed
  configuration with a plain store and immediately reclaims the previous…
- [Medium] The freshly kmemdup()'ed vxlan_config is published with a
  plain assignment (`vxlan->cfg = new_cfg;`,…
- [Low] vxlan_encap_bypass() (drivers/net/vxlan/vxlan_core.c) newly
  treats `dst_vxlan->cfg == NULL` as a possible state (`if…
- [Low] vxlan_fdb_get() (drivers/net/vxlan/vxlan_core.c:1403-1421) adds
  NULL handling for vxlan->cfg only on the branch where NDA_VNI is…

--

vxlan: dynamically allocate struct vxlan_config

This moves struct vxlan_config out of struct vxlan_dev into a
dynamically allocated pointer vxlan->cfg.  vxlan_changelink() and
vxlan_dev_configure() now allocate a new config and free the old one.
A few questions below about the intermediate state this commit leaves
behind.
quoted hunk ↗ jump to hunk
diff --git a/drivers/net/vxlan/vxlan_core.c b/drivers/net/vxlan/vxlan_core.c
index 9856ec710fa1d..5bc3475886b09 100644
--- a/drivers/net/vxlan/vxlan_core.c
+++ b/drivers/net/vxlan/vxlan_core.c
[ ... ]
quoted hunk ↗ jump to hunk
@@ -1396,18 +1402,23 @@ static int vxlan_fdb_get(struct sk_buff *skb,
 			 struct netlink_ext_ack *extack)
 {
 	struct vxlan_dev *vxlan = netdev_priv(dev);
+	const struct vxlan_config *cfg;
 	struct vxlan_fdb *f;
 	__be32 vni;
 	int err;
 
+	cfg = vxlan->cfg;
+
 	if (tb[NDA_VNI])
 		vni = cpu_to_be32(nla_get_u32(tb[NDA_VNI]));
+	else if (cfg)
+		vni = cfg->vni;
 	else
-		vni = vxlan->default_dst.remote_vni;
+		return -ENODEV;
 
 	rcu_read_lock();
 
-	f = vxlan_find_mac_rcu(vxlan, &vxlan->cfg, addr, vni);
+	f = vxlan_find_mac_rcu(vxlan, cfg, addr, vni);
[Severity: Low]
Are the two branches here meant to disagree about whether cfg can be
NULL?

When user space passes NDA_VNI the new -ENODEV check is skipped, and the
same possibly-NULL pointer is handed to vxlan_find_mac_rcu(), which
dereferences it right away:

	if (!(cfg->flags & VXLAN_F_COLLECT_METADATA))
		key.vni = cfg->vni;

Tracing it through, vxlan->cfg is set by vxlan_config_apply() from
vxlan_dev_configure() before register_netdevice(), and only cleared by
vxlan_free_dev() (the register_netdevice() failure path, or the
priv_destructor after unregistration), so a device reachable via
__dev_get_by_index() under RTNL always has a non-NULL cfg and the
-ENODEV branch looks like dead code.

Would it be clearer to drop the NULL test rather than have half of the
function defend against a state the other half assumes cannot happen?
The same asymmetry appears in vxlan_open() and vxlan_sock_release(),
where cfg is tested for NULL after vxlan_sock_add()/__vxlan_sock_add()
have already dereferenced it unconditionally.  It is still present at
the end of the series.

[ ... ]
quoted hunk ↗ jump to hunk
@@ -1692,7 +1704,8 @@ static int vxlan_rcv(struct sock *sk, struct sk_buff *skb)
 		goto drop;
 	}
 
-	cfg = &vxlan->cfg;
+	cfg = vxlan->cfg;
+
 	if (vh->vx_flags & cfg->reserved_bits.vx_flags ||
 	    vh->vx_vni & cfg->reserved_bits.vx_vni) {
[Severity: High]
Can this read freed memory?

vxlan_rcv() runs in softirq for every received frame and now caches a
plain pointer to a kmalloc()'ed object, while the only writer,
vxlan_config_apply(), releases the old object synchronously under RTNL:

	old_cfg = vxlan->cfg;
	vxlan->cfg = new_cfg;
	kfree(old_cfg);

So:

CPU0 (softirq)                     CPU1 (RTNL, ip link set ... type vxlan)
vxlan_rcv()
  cfg = vxlan->cfg;
                                   vxlan_changelink()
                                     vxlan_config_apply()
                                       vxlan->cfg = new_cfg;
                                       kfree(old_cfg);
  cfg->reserved_bits.vx_flags        <- freed

The same pattern applies to vxlan_vs_find_vni(), which dereferences
another device's cfg on every RX VNI lookup, and to vxlan_xmit(), which
propagates the cached pointer into arp_reduce(), neigh_reduce(),
vxlan_find_mac_tx(), vxlan_xmit_one() and vxlan_vnifilter_count().

The following patch in the series, "vxlan: convert configuration to RCU
protection", adds a struct rcu_head to struct vxlan_config, switches
publication to rcu_assign_pointer() plus kfree_rcu() and converts the
readers to rcu_dereference() inside RCU read sections, so the end state
of the series is fine.  Would it be better to fold that change in here,
or to at least use kfree_rcu() in this commit, so that this intermediate
commit is not a bisect or backport hazard on its own?

[ ... ]
quoted hunk ↗ jump to hunk
@@ -2284,7 +2299,7 @@ static void vxlan_encap_bypass(struct sk_buff *skb, struct vxlan_dev *src_vxlan,
 			       const struct vxlan_config *src_cfg,
 			       __be32 vni, bool snoop)
 {
-	const struct vxlan_config *dst_cfg = &dst_vxlan->cfg;
+	const struct vxlan_config *dst_cfg;
 	union vxlan_addr loopback;
 	unsigned int len = skb->len;
 	struct net_device *dev = dst_vxlan->dev;
@@ -2305,7 +2320,8 @@ static void vxlan_encap_bypass(struct sk_buff *skb, struct vxlan_dev *src_vxlan,
 	}
 
 	rcu_read_lock();
-	if (unlikely(!(dev->flags & IFF_UP))) {
+	dst_cfg = dst_vxlan->cfg;
+	if (unlikely(!dst_cfg || !(dev->flags & IFF_UP))) {
 		kfree_skb_reason(skb, SKB_DROP_REASON_DEV_READY);
 		goto drop;
 	}
[Severity: Low]
If the !dst_cfg case is really possible, does the drop label then
dereference NULL?

The goto lands on the shared drop label, which forwards the same pointer
on:

drop:
		dev_dstats_rx_dropped(dev);
		vxlan_vnifilter_count(dst_vxlan, dst_cfg, vni, NULL,
				      VXLAN_VNI_STATS_RX_DROPS, 0);

and this same commit removes the NULL tolerance from that helper (see
the vxlan_vnifilter_count() hunk below), so it starts with
cfg->flags.

As far as I can tell dst_vxlan->cfg cannot be NULL for a device that is
reachable through the VNI hash: devices are only inserted from
vxlan_vs_add_dev()/vxlan_vs_add_vnigrp() during ndo_open, and cfg is
populated before register_netdevice() and cleared only by
vxlan_free_dev().  So the new test appears unreachable, but caller and
callee end up with opposite contracts.

A later patch in the series, "vxlan: remove default_dst and use
vxlan_config and lowerdev", changes the drop label to guard the call
with if (dst_cfg).  Could that guard come with this hunk instead, or
could the !dst_cfg test be dropped here?

[ ... ]
quoted hunk ↗ jump to hunk
@@ -2771,7 +2787,7 @@ static netdev_tx_t vxlan_xmit(struct sk_buff *skb, struct net_device *dev)
 	u32 nhid = 0;
 	u32 flags;
 
-	cfg = &vxlan->cfg;
+	cfg = vxlan->cfg;
 	flags = cfg->flags;
 	default_vni = cfg->vni;
 	saddr_family = cfg->saddr.sa.sa_family;
[ ... ]
quoted hunk ↗ jump to hunk
@@ -2890,12 +2906,15 @@ static void vxlan_cleanup(struct timer_list *t)
 {
 	struct vxlan_dev *vxlan = timer_container_of(vxlan, t, age_timer);
 	unsigned long next_timer = jiffies + FDB_AGE_INTERVAL;
+	const struct vxlan_config *cfg;
 	struct vxlan_fdb *f;
 
 	if (!netif_running(vxlan->dev))
 		return;
 
 	rcu_read_lock();
+	cfg = vxlan->cfg;
+
 	hlist_for_each_entry_rcu(f, &vxlan->fdb_list, fdb_node) {
 		unsigned long timeout;
 
@@ -2905,7 +2924,7 @@ static void vxlan_cleanup(struct timer_list *t)
 		if (f->flags & NTF_EXT_LEARNED)
 			continue;
 
-		timeout = READ_ONCE(f->updated) + vxlan->cfg.age_interval * HZ;
+		timeout = READ_ONCE(f->updated) + cfg->age_interval * HZ;
[Severity: High]
Does the rcu_read_lock() here buy anything while the writer uses plain
kfree()?

cfg is loaded once and read across the whole FDB walk in the ageing
timer softirq, but vxlan_config_apply() frees the old config
immediately, without waiting for a grace period, so this looks like the
same use-after-free window as in vxlan_rcv()/vxlan_xmit().

[ ... ]
quoted hunk ↗ jump to hunk
@@ -3984,7 +4040,9 @@ static void vxlan_config_apply(struct net_device *dev,
 	needed_headroom += vxlan_headroom(flags);
 	dev->needed_headroom = needed_headroom;
 
-	memcpy(&vxlan->cfg, conf, sizeof(*conf));
+	old_cfg = vxlan->cfg;
+	vxlan->cfg = new_cfg;
+	kfree(old_cfg);
 }
[Severity: Medium]
Should the publication of new_cfg use release semantics?

The object comes straight out of kmemdup() and is published with a plain
store, while vxlan_rcv(), vxlan_xmit(), vxlan_vs_find_vni(),
vxlan_cleanup() and vxlan_fill_metadata_dst() consume it with plain
loads.  There is no rcu_assign_pointer()/smp_store_release() on the
write side and no rcu_dereference()/READ_ONCE() on the read side.

On weakly ordered architectures, can a softirq reader observe the new
pointer before the kmemdup() payload stores are visible, and then act on
stale cfg->flags, cfg->dst_port or cfg->reserved_bits?

The field is also declared as a plain pointer rather than __rcu (see the
include/net/vxlan.h hunk), so sparse cannot flag the missing
primitives.  The follow-up patch "vxlan: convert configuration to RCU
protection" adds the __rcu annotation and rcu_assign_pointer(), so this
gap exists only at this commit.
quoted hunk ↗ jump to hunk
diff --git a/drivers/net/vxlan/vxlan_vnifilter.c b/drivers/net/vxlan/vxlan_vnifilter.c
index f7b60855a05f4..4fba3579774a7 100644
--- a/drivers/net/vxlan/vxlan_vnifilter.c
+++ b/drivers/net/vxlan/vxlan_vnifilter.c
@@ -178,7 +178,7 @@ void vxlan_vnifilter_count(struct vxlan_dev *vxlan,
 {
 	struct vxlan_vni_node *vnode;
 
-	if (!cfg || !(cfg->flags & VXLAN_F_VNIFILTER))
+	if (!(cfg->flags & VXLAN_F_VNIFILTER))
 		return;
[Severity: Low]
Is removing the !cfg test here intentional in the same patch that adds a
NULL-tolerant caller?

vxlan_encap_bypass() now treats dst_vxlan->cfg == NULL as a reachable
state and still reaches this helper through its drop label, so the two
sides of the call disagree about the contract.
quoted hunk ↗ jump to hunk
diff --git a/include/net/vxlan.h b/include/net/vxlan.h
index d323f91af2364..10f56b5f5206c 100644
--- a/include/net/vxlan.h
+++ b/include/net/vxlan.h
@@ -302,7 +302,7 @@ struct vxlan_dev {
 	struct gro_cells  gro_cells;
 	unsigned long	  flags;
 
-	struct vxlan_config	cfg;
+	struct vxlan_config	*cfg;
[Severity: Medium]
Since this pointer is already read locklessly from softirq contexts in
this commit, would annotating it __rcu here (as the following patch
does) make the missing rcu_dereference() sites visible to sparse right
away?

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260906180111.1973188-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