Thread (28 messages) flat view 28 messages, 3 authors, 3d ago

Re: [PATCH net-next 6/9] vxlan: dynamically allocate struct vxlan_config

From: netdev-bot+sashiko@kernel.org
Date: 2026-09-07 06:11:26

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 allocated config with a
  plain store and immediately reclaims the old one with a bare kfree()…
- [Medium] The new configuration pointer is published with a plain store
  (vxlan_core.c:4038 `vxlan->cfg = new_cfg;`) right after the payload is…
- [Low] vxlan_encap_bypass() adds a `!dst_cfg` NULL test whose error
  branch (`goto drop`) lands on the `drop:` label, and that label passes…
- [Low] The patch introduces two mutually exclusive assumptions about
  vxlan->cfg for a live/registered netdev, on the same code paths.

--

vxlan: dynamically allocate struct vxlan_config

This moves struct vxlan_config out of struct vxlan_dev into a separately
allocated object referenced by vxlan->cfg, with vxlan_changelink() and
vxlan_dev_configure() allocating a new config and freeing the old one.
It is described as preparation for converting vxlan->cfg to RCU
protection in the following patch.
quoted hunk ↗ jump to hunk
diff --git a/drivers/net/vxlan/vxlan_core.c b/drivers/net/vxlan/vxlan_core.c
index 0fcc7282e69d..af320173a1e7 100644
--- a/drivers/net/vxlan/vxlan_core.c
+++ b/drivers/net/vxlan/vxlan_core.c
@@ -110,20 +110,23 @@ static struct vxlan_dev *vxlan_vs_find_vni(struct vxlan_sock *vs,
 		vni = 0;
 
 	hlist_for_each_entry_rcu(node, vni_head(vs, vni), hlist) {
+		const struct vxlan_config *cfg;
+
 		if (!node->vxlan)
 			continue;
+
+		cfg = node->vxlan->cfg;
+
 		vnode = NULL;
-		if (node->vxlan->cfg.flags & VXLAN_F_VNIFILTER) {
+		if (cfg->flags & VXLAN_F_VNIFILTER) {
 			vnode = vxlan_vnifilter_lookup(node->vxlan, vni);
 			if (!vnode)
 				continue;
-		} else if (node->vxlan->default_dst.remote_vni != vni) {
+		} else if (cfg->vni != vni) {
 			continue;
 		}
[Severity: High]
This per-packet RX lookup now walks other devices' cfg pointers with a
plain load.  Combined with the plain kfree() added in vxlan_config_apply()
below, can this dereference freed memory?

CPU0 (RTNL, ip link set ... type vxlan ...)
  vxlan_changelink()
    vxlan_config_apply()
      old_cfg = vxlan->cfg;
      vxlan->cfg = new_cfg;
      kfree(old_cfg);          <-- no grace period

CPU1 (RX softirq)
  vxlan_rcv()
    vxlan_vs_find_vni()
      cfg = node->vxlan->cfg;  <-- may already be old_cfg
      if (cfg->flags & VXLAN_F_VNIFILTER)   <-- use-after-free

[ ... ]
quoted hunk ↗ jump to hunk
@@ -1510,6 +1521,7 @@ static bool __vxlan_sock_release_prep(struct vxlan_sock *vs)
 
 static void vxlan_sock_release(struct vxlan_dev *vxlan)
 {
+	const struct vxlan_config *cfg = vxlan->cfg;
 	struct vxlan_sock *sock4 = rtnl_dereference(vxlan->vn4_sock);
 #if IS_ENABLED(CONFIG_IPV6)
 	struct vxlan_sock *sock6 = rtnl_dereference(vxlan->vn6_sock);
@@ -1519,7 +1531,7 @@ static void vxlan_sock_release(struct vxlan_dev *vxlan)
 
 	RCU_INIT_POINTER(vxlan->vn4_sock, NULL);
 
-	if (vxlan->cfg.flags & VXLAN_F_VNIFILTER)
+	if (cfg && (cfg->flags & VXLAN_F_VNIFILTER))
 		vxlan_vs_del_vnigrp(vxlan);
 	else
 		vxlan_vs_del_dev(vxlan);
[Severity: Low]
Can cfg actually be NULL here?  The caller vxlan_stop() dereferences the
same pointer unconditionally a couple of statements earlier:

vxlan_stop()
  vxlan_multicast_leave(vxlan)
    if (vxlan->cfg->flags & VXLAN_F_VNIFILTER)   <-- no NULL check
  timer_delete_sync(&vxlan->age_timer);
  vxlan_flush(vxlan, &desc);
  vxlan_sock_release(vxlan)
    if (cfg && (cfg->flags & VXLAN_F_VNIFILTER)) <-- NULL check

The same disagreement shows up in several other pairs added by this patch:
vxlan_open() tests "cfg && cfg->age_interval" after vxlan_sock_add() and
__vxlan_sock_add() already read cfg->flags unguarded, vxlan_uninit() tests
"cfg &&" while its ndo_init counterpart vxlan_init() uses cfg->flags
directly, and vxlan_fdb_get() returns -ENODEV for a NULL cfg while
vxlan_fdb_parse() on the same registered device dereferences cfg->dst_port
and cfg->vni without a check.

Since vxlan->cfg is cleared only in vxlan_free_dev(), the priv_destructor,
which runs from free_netdev() after the device is unregistered (and
rtnl_newlink_create() calls free_netdev() directly on ->newlink failure
without invoking ndo_uninit), it looks like none of the netdev ops can
observe NULL, making these guards and the new -ENODEV path dead code.
Would it be clearer to drop the checks and keep the invariant uniform?
These mixed assumptions also remain 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) {
 		/* If the header uses bits besides those enabled by the
[Severity: Medium]
The pointer is loaded plainly here and used for many dereferences
(cfg->reserved_bits, cfg->flags) while vxlan_config_apply() publishes the
kmemdup()'ed replacement with a plain store.

Without a release barrier on the writer side (rcu_assign_pointer() or
smp_store_release()) and a dependency-ordering load on the reader side
(rcu_dereference()/READ_ONCE()), can a reader on a weakly ordered CPU see
the new pointer before the kmemdup'ed payload stores are visible, and act
on stale flags/reserved_bits/dst_port/saddr values?

The field is also declared without __rcu in include/net/vxlan.h, so sparse
cannot flag the missing accessors.  The following patch, "vxlan: convert
configuration to RCU protection", adds __rcu plus
rcu_assign_pointer()/rcu_dereference() at these sites, so would it be
reasonable to fold the annotation into this patch instead of leaving an
intermediate commit without ordering?

[ ... ]
quoted hunk ↗ jump to hunk
@@ -2278,7 +2293,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;
@@ -2299,7 +2314,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 new !dst_cfg branch is ever taken, where does it land?  The goto
jumps to the drop label at the end of the same function, which passes the
same NULL dst_cfg straight into vxlan_vnifilter_count():

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

and this patch also removes the "!cfg ||" guard from that helper in
vxlan_vnifilter.c, so it now starts with:

	if (!(cfg->flags & VXLAN_F_VNIFILTER))
		return;

That would be a NULL dereference in softirq context.  The check appears to
be unreachable today, since vxlan->cfg is only cleared in vxlan_free_dev()
after the device is gone from the VNI hash, so is the new test needed at
all?  A later patch in the series, "vxlan: remove default_dst and use
vxlan_config and lowerdev", wraps the drop-path call in "if (dst_cfg)",
which suggests the intent was for the check to be live.

[ ... ]
quoted hunk ↗ jump to hunk
@@ -2765,7 +2781,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;
[Severity: High]
This pointer is cached at function entry, outside any rcu_read_lock() in
this commit, and then handed to arp_reduce(), neigh_reduce(),
vxlan_xmit_one(), vxlan_xmit_nh(), vxlan_mdb_xmit() and
vxlan_vnifilter_count() for the duration of the transmit.  Can a
concurrent changelink free it underneath?

Before this patch the config was embedded in vxlan_dev and updated in
place, so a racing reader could only see a torn config.  Now the reader can
see freed slab memory, and vxlan_xmit_one() feeds cfg->saddr, cfg->ttl,
cfg->tos, cfg->label and cfg->dst_port into the encapsulation headers.

[ ... ]
quoted hunk ↗ jump to hunk
@@ -2884,12 +2900,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;
 
@@ -2899,7 +2918,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]
The rcu_read_lock() here gives no protection against the plain kfree() in
vxlan_config_apply(), so can the timer softirq read cfg->age_interval from
a freed object across the whole FDB walk?

[ ... ]
quoted hunk ↗ jump to hunk
@@ -3247,14 +3276,18 @@ static int vxlan_change_mtu(struct net_device *dev, int new_mtu)
[ ... ]
quoted hunk ↗ jump to hunk
@@ -3381,6 +3417,14 @@ static void vxlan_offload_rx_ports(struct net_device *dev, bool push)
 	}
 }
 
+static void vxlan_free_dev(struct net_device *dev)
+{
+	struct vxlan_dev *vxlan = netdev_priv(dev);
+
+	kfree(vxlan->cfg);
+	vxlan->cfg = NULL;
+}
+
[ ... ]
quoted hunk ↗ jump to hunk
@@ -3978,7 +4034,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: High]
This is the writer side of the use-after-free discussed above.  RTNL is
held here, but none of the readers (vxlan_xmit(), vxlan_rcv(),
vxlan_vs_find_vni(), vxlan_cleanup(), vxlan_fill_metadata_dst(),
mlxsw_sp_switchdev_vxlan_work_prepare()) hold RTNL, and there is no grace
period, refcount or shared lock between them.  Can the old config be freed
while an in-flight packet still dereferences it?

Since the commit message says this prepares for the RCU conversion in the
following patch, would it make sense to use kfree_rcu() (adding the
rcu_head in this patch) so the intermediate commit is not a bisect and
backport hazard?

[Severity: Medium]
Separately from the lifetime problem, this publishes a pointer to freshly
kmemdup()'ed data with a plain store.  Should this be rcu_assign_pointer()
(or at least smp_store_release()) so readers cannot observe the pointer
before the payload writes?

[ ... ]
quoted hunk ↗ jump to hunk
@@ -3987,13 +4045,18 @@ static int vxlan_dev_configure(struct net *src_net, struct net_device *dev,
 {
 	struct vxlan_dev *vxlan = netdev_priv(dev);
 	struct net_device *lowerdev;
+	struct vxlan_config *new_cfg;
 	int ret;
 
 	ret = vxlan_config_validate(src_net, conf, &lowerdev, vxlan, extack);
 	if (ret)
 		return ret;
 
-	vxlan_config_apply(dev, conf, lowerdev, src_net, false);
+	new_cfg = kmemdup(conf, sizeof(*conf), GFP_KERNEL);
+	if (!new_cfg)
+		return -ENOMEM;
+
+	vxlan_config_apply(dev, new_cfg, lowerdev, src_net, false);
 
 	return 0;
 }
[ ... ]
quoted hunk ↗ jump to hunk
diff --git a/drivers/net/vxlan/vxlan_vnifilter.c b/drivers/net/vxlan/vxlan_vnifilter.c
index c46d57164936..8f6c01930ace 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]
Dropping the "!cfg ||" guard here makes every caller responsible for a
non-NULL cfg, which conflicts with the new !dst_cfg branch added in
vxlan_encap_bypass() that reaches its drop label with dst_cfg still NULL
and then calls this helper.  Should the two changes agree one way or the
other?
 
 	if (vninode) {
[ ... ]

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