[PATCH net 1/3] vxlan: vnifilter: limit the VNI range of a single request

From: Ali Firas

Date: Wed Sep 09 2026 - 05:45:06 EST


VXLAN_VNIFILTER_ENTRY_START and VXLAN_VNIFILTER_ENTRY_END are parsed
without any bound on how far apart they are, so a single RTM_NEWTUNNEL
message can ask for the whole 24-bit VNI space. vxlan_vni_add_del() then
loops over that span creating one VNI node and one per-CPU stats block
per iteration, all under rtnl_lock.

Two things follow from that, both reachable by an unprivileged user in a
user+network namespace, since adding VNIs only requires CAP_NET_ADMIN in
the network namespace's user namespace:

- the allocation is unbounded. Each VNI costs 128 bytes of slab plus
64 bytes per possible CPU, so a full in-range request costs roughly
2.1 GiB + 1 GiB per possible CPU. Measured in a QEMU guest on 2, 4
and 8 CPU configurations, every one of them ends in a global OOM
with the allocating task in vxlan_vnifilter_process(). No errno is
returned because the calling process is itself OOM-killed, and the
OOM killer also killed unrelated root-owned processes.

- rtnl_lock is held for the entire loop. rtnl is global rather than
per-netns, so unrelated network configuration blocks everywhere for
as long as the request runs. Measured with a plain "ip link add
dummy0 type dummy" in a different network namespace: it takes 0.011
s normally, 4.472 s while a 1,000,000 VNI request runs, and during a
full-range request it never completes at all.

Cap the span of one request at 4096 VNIs. The limit is on a single
request, not on how many VNIs a device may hold: a device can still be
populated with the whole VNI space, it just takes more than one message.
The value follows from how the interface is used in practice, on bridged
VXLAN devices where the VNI is derived from the VLAN and so cannot
exceed the 4094 usable VLAN IDs.

The limit is written as a driver-local constant rather than reusing
VLAN_N_VID. The two numbers coincide today, but a bound on a VXLAN
netlink request is not a count of VLAN IDs, and tying them together
would make a change to one silently change the other.

The check sits in vxlan_process_vni_filter(), where the span is known
and before any VNI is created, so it rejects the request before any work
is done. Both RTM_NEWTUNNEL and RTM_DELTUNNEL reach vxlan_vni_add_del()
through this one function, so a single check covers add and delete.

The span is inclusive, so START=0 END=4095 is 4096 VNIs and is accepted,
while START=0 END=4096 is 4097 and is rejected. A request carrying only
END has START default to 0 and is bounded the same way. A start above
the end selects no VNI at all and is deliberately left behaving as it
does today, rather than being turned into an error by unsigned
wraparound.

Fixes: f9c4bb0b245c ("vxlan: vni filtering support on collect metadata device")
Assisted-by: LLM
Signed-off-by: Ali Firas <alishmery18@xxxxxxxxx>
---
drivers/net/vxlan/vxlan_vnifilter.c | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)

diff --git a/drivers/net/vxlan/vxlan_vnifilter.c b/drivers/net/vxlan/vxlan_vnifilter.c
index dd94085e0886..f18ce0e1e741 100644
--- a/drivers/net/vxlan/vxlan_vnifilter.c
+++ b/drivers/net/vxlan/vxlan_vnifilter.c
@@ -17,6 +17,14 @@

#include "vxlan_private.h"

+/* Maximum number of VNIs a single RTM_NEWTUNNEL or RTM_DELTUNNEL request may
+ * span. VNI filtering is mainly used on bridged VXLAN devices where the VNI
+ * is derived from the VLAN, so a span wider than the VLAN ID space has no
+ * practical use, while an unbounded span lets one netlink message create up
+ * to 2^24 VNIs under rtnl_lock.
+ */
+#define VXLAN_VNI_FILTER_RANGE_MAX 4096
+
static inline int vxlan_vni_cmp(struct rhashtable_compare_arg *arg,
const void *ptr)
{
@@ -869,6 +877,17 @@ static int vxlan_process_vni_filter(struct vxlan_dev *vxlan,
return -EINVAL;
}

+ /* Only bound a well-formed range; a start above the end selects no
+ * VNI at all and is left behaving as before.
+ */
+ if (vni_end >= vni_start &&
+ vni_end - vni_start >= VXLAN_VNI_FILTER_RANGE_MAX) {
+ NL_SET_ERR_MSG_ATTR_FMT(extack, nlvnifilter,
+ "VNI range spans more than %u VNIs",
+ VXLAN_VNI_FILTER_RANGE_MAX);
+ return -EINVAL;
+ }
+
if (vattrs[VXLAN_VNIFILTER_ENTRY_GROUP]) {
group.sin.sin_addr.s_addr =
nla_get_in_addr(vattrs[VXLAN_VNIFILTER_ENTRY_GROUP]);
--
2.53.0