From: netdev-bot+sashiko@kernel.org
To: edumazet@google.com
Cc: davem@davemloft.net, kuba@kernel.org, pabeni@redhat.com,
horms@kernel.org, andrew+netdev@lunn.ch, idosch@nvidia.com,
kuniyu@google.com, netdev@vger.kernel.org,
eric.dumazet@gmail.com
Subject: Re: [PATCH v2 net-next 5/8] vxlan: dynamically allocate struct vxlan_config
Date: Tue, 08 Sep 2026 18:03:54 +0000 [thread overview]
Message-ID: <178889063414.219967.17640576039088216642@kernel.org> (raw)
In-Reply-To: <20260906180111.1973188-6-edumazet@google.com>
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.
> 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
[ ... ]
> @@ -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.
[ ... ]
> @@ -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?
[ ... ]
> @@ -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?
[ ... ]
> @@ -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;
[ ... ]
> @@ -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().
[ ... ]
> @@ -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.
> 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.
> 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
next prev parent reply other threads:[~2026-09-08 18:03 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-06 18:01 [PATCH v2 net-next 0/8] vxlan: convert configuration to RCU and drop RTNL in vxlan_fill_info() Eric Dumazet
2026-09-06 18:01 ` [PATCH v2 net-next 1/8] vxlan: initialize _md in vxlan_xmit_one() Eric Dumazet
2026-09-06 18:01 ` [PATCH v2 net-next 2/8] vxlan: vnifilter: use list_for_each_entry_rcu() in vxlan_vnifilter_dump_dev() Eric Dumazet
2026-09-08 18:03 ` netdev-bot+sashiko
2026-09-06 18:01 ` [PATCH v2 net-next 3/8] vxlan: pass vxlan_config pointer to helper functions Eric Dumazet
2026-09-06 18:01 ` [PATCH v2 net-next 4/8] vxlan: move VXLAN_F_MDB to struct vxlan_dev flags Eric Dumazet
2026-09-06 18:01 ` [PATCH v2 net-next 5/8] vxlan: dynamically allocate struct vxlan_config Eric Dumazet
2026-09-08 18:03 ` netdev-bot+sashiko [this message]
2026-09-10 1:35 ` Jakub Kicinski
2026-09-11 2:15 ` Eric Dumazet
2026-09-06 18:01 ` [PATCH v2 net-next 6/8] vxlan: convert configuration to RCU protection Eric Dumazet
2026-09-08 18:03 ` netdev-bot+sashiko
2026-09-06 18:01 ` [PATCH v2 net-next 7/8] vxlan: remove default_dst and use vxlan_config and lowerdev Eric Dumazet
2026-09-08 18:03 ` netdev-bot+sashiko
2026-09-06 18:01 ` [PATCH v2 net-next 8/8] vxlan: no longer rely on RTNL in vxlan_fill_info() Eric Dumazet
2026-09-08 18:03 ` netdev-bot+sashiko
2026-09-10 1:40 ` [PATCH v2 net-next 0/8] vxlan: convert configuration to RCU and drop " patchwork-bot+netdevbpf
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=178889063414.219967.17640576039088216642@kernel.org \
--to=netdev-bot+sashiko@kernel.org \
--cc=andrew+netdev@lunn.ch \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=eric.dumazet@gmail.com \
--cc=horms@kernel.org \
--cc=idosch@nvidia.com \
--cc=kuba@kernel.org \
--cc=kuniyu@google.com \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.