* [PATCH net-next v5 1/8] vxlan: vnifilter: reject VNIs outside the 24-bit space
2026-11-14 22:14 [PATCH net-next v5 0/8] vxlan: vnifilter: bound a single request and account per-VNI memory Ali Firas
@ 2026-11-14 22:14 ` Ali Firas
2026-11-14 22:14 ` [PATCH net-next v5 2/8] vxlan: vnifilter: clamp the dumped VNI range to the request limit Ali Firas
` (6 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Ali Firas @ 2026-11-14 22:14 UTC (permalink / raw)
To: netdev, idosch
Cc: kuba, pabeni, davem, edumazet, andrew+netdev, horms, razor,
roopa, kuniyu, petrm, aroulin, shuah, linux-kselftest,
linux-kernel, Ali Firas
VXLAN_VNIFILTER_ENTRY_START and VXLAN_VNIFILTER_ENTRY_END are bare
NLA_U32, so neither is bounded before vxlan_process_vni_filter() hands
them to vxlan_vni_add_del():
int v, err = 0;
...
for (v = start_vni; v <= end_vni; v++)
v is int and end_vni is __u32, so the comparison is done unsigned. A
request carrying only START=0xffffffff has vni_start == vni_end ==
0xffffffff and looks like a single VNI; v is then -1, the comparison
promotes it to 0xffffffff and passes, v++ makes v 0, and the loop walks
the space upwards from there, creating a VNI node and a per-CPU stats
block per iteration under rtnl_lock. Any range ending at 0xffffffff
behaves the same way.
Separately, a VNI at or above VXLAN_N_VID is accepted and stored even
though the VXLAN header carries only 24 bits: vxlan_vni_field() shifts
without masking, so such an entry keeps its own rhashtable slot while
being truncated on the wire.
Range-validate both attributes against the 24-bit space, as vxlan_mdb.c
already does for its own VNI attributes. Since commit 599d0e97610f
("vxlan: vnifilter: validate the VXLAN_VNIFILTER_ENTRY nest") linked the
nest to this policy, an out-of-range endpoint is rejected during netlink
policy validation, before vxlan_process_vni_filter() runs and before
anything is allocated. Without that commit the range would be checked
one entry at a time, by the nla_parse_nested() in
vxlan_process_vni_filter(), after the earlier entries of the message had
been applied. How many VNIs a single in-range request may span is a
separate question, bounded later in this series by "vxlan: vnifilter:
bound the number of VNIs one request may touch"; that limit is not a
policy check and runs in the handler.
Make the loop counter u32 as well. With the range bounded it is no
longer what keeps the loop finite, but it drops the undefined signed
overflow past INT_MAX and matches the u32 vni that vxlan_vni_add() and
vxlan_vni_del() already take.
Found by code review: vxlan_mdb.c range-checks its VNI attributes
against VXLAN_N_VID, and the vnifilter policy did not. In QEMU on
net-next (production build, 2 vCPU), VNI 16777216 is accepted and
stored, and a lone START=0xffffffff wraps the loop counter and keeps
allocating VNI nodes under rtnl_lock.
This targets net-next without a Fixes tag and is not a backport
candidate. Reviewing the earlier posting of this range check alone,
Jakub said it was either a security fix needing a stronger check or
something making things slightly better that did not deserve Fixes or
stable.
Link: https://lore.kernel.org/netdev/20260905123900.23984eda@kernel.org/
Signed-off-by: Ali Firas <alishmery18@gmail.com>
---
v5: state the dependency on the applied nest patch, named by commit id
(Paolo and review of v3); name the cap patch by subject instead of "the
next patch"; the changelog no longer calls this "not a fix" and says how
the gap was found and what it did in QEMU (review of v4, sinfo bot);
policy lines wrapped to 80 columns; rebased. v4: no functional change;
rebased onto net-next.
drivers/net/vxlan/vxlan_vnifilter.c | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/drivers/net/vxlan/vxlan_vnifilter.c b/drivers/net/vxlan/vxlan_vnifilter.c
index 124ba47758e9..2b02f448c3e9 100644
--- a/drivers/net/vxlan/vxlan_vnifilter.c
+++ b/drivers/net/vxlan/vxlan_vnifilter.c
@@ -491,9 +491,15 @@ static int vxlan_vnifilter_dump(struct sk_buff *skb, struct netlink_callback *cb
return err;
}
+static const struct netlink_range_validation vni_filter_vni_range = {
+ .max = VXLAN_N_VID - 1,
+};
+
static const struct nla_policy vni_filter_entry_policy[VXLAN_VNIFILTER_ENTRY_MAX + 1] = {
- [VXLAN_VNIFILTER_ENTRY_START] = { .type = NLA_U32 },
- [VXLAN_VNIFILTER_ENTRY_END] = { .type = NLA_U32 },
+ [VXLAN_VNIFILTER_ENTRY_START] =
+ NLA_POLICY_FULL_RANGE(NLA_U32, &vni_filter_vni_range),
+ [VXLAN_VNIFILTER_ENTRY_END] =
+ NLA_POLICY_FULL_RANGE(NLA_U32, &vni_filter_vni_range),
[VXLAN_VNIFILTER_ENTRY_GROUP] = NLA_POLICY_EXACT_LEN(sizeof_field(struct iphdr, daddr)),
[VXLAN_VNIFILTER_ENTRY_GROUP6] = NLA_POLICY_EXACT_LEN(sizeof(struct in6_addr)),
};
@@ -914,7 +920,8 @@ static int vxlan_vni_add_del(struct vxlan_dev *vxlan, __u32 start_vni,
int cmd, struct netlink_ext_ack *extack)
{
struct vxlan_vni_group *vg;
- int v, err = 0;
+ int err = 0;
+ u32 v;
vg = rtnl_dereference(vxlan->vnigrp);
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH net-next v5 2/8] vxlan: vnifilter: clamp the dumped VNI range to the request limit
2026-11-14 22:14 [PATCH net-next v5 0/8] vxlan: vnifilter: bound a single request and account per-VNI memory Ali Firas
2026-11-14 22:14 ` [PATCH net-next v5 1/8] vxlan: vnifilter: reject VNIs outside the 24-bit space Ali Firas
@ 2026-11-14 22:14 ` Ali Firas
2026-11-14 22:14 ` [PATCH net-next v5 3/8] vxlan: vnifilter: bound the number of VNIs one request may touch Ali Firas
` (5 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Ali Firas @ 2026-11-14 22:14 UTC (permalink / raw)
To: netdev, idosch
Cc: kuba, pabeni, davem, edumazet, andrew+netdev, horms, razor,
roopa, kuniyu, petrm, aroulin, shuah, linux-kselftest,
linux-kernel, Ali Firas
vxlan_vnifilter_dump_dev() coalesces a contiguous run of VNIs sharing a
remote into one VXLAN_VNIFILTER_ENTRY, and packs as many entries into a
netlink message as the skb holds, with no bound on either. The patch
that follows, "vxlan: vnifilter: bound the number of VNIs one request
may touch", bounds a single request to VXLAN_VNI_FILTER_MSG_MAX VNIs
summed over its entries; without a matching bound on the dump, a device
holding more than that -- populated by several requests -- would dump a
run, or a message, that the input path then refuses on replay.
Introduce VXLAN_VNI_FILTER_MSG_MAX here and clamp the dump to it first, so
the series never passes through a commit where the kernel reports a range
its own input path rejects. Bound the two ways the dump can exceed the
limit:
- end a coalesced run once it reaches the limit, in the merge condition,
even when the next VNI is contiguous and shares the remote; and
- carry a per-message VNI budget, ending the message once its entries
would sum past the limit, the same way the loop already ends it when
the skb is full.
cb->args[1] continues to count the VNI nodes already dumped and is
advanced only when a completed entry is written, so the budget break and
the skb-full break share one resume path; a run stays a gapless block,
so while the VNI set does not change between messages the dump never
drops a VNI and never emits one twice. A VNI added or deleted between
messages is flagged with NLM_F_DUMP_INTR, as before.
Signed-off-by: Ali Firas <alishmery18@gmail.com>
---
v5: the VXLAN_VNI_FILTER_MSG_MAX comment states what the dump does
instead of pointing at "a later patch", and the changelog names the cap
patch by subject (review of v4); the no-drop/no-repeat claim is scoped
to a VNI set that does not change between messages; one line wrapped to
80 columns. v4: moved before the input cap; now introduces
VXLAN_VNI_FILTER_MSG_MAX and adds a per-message VNI budget (v3 clamped
per entry only), so the dump never emits a message the input path
rejects.
drivers/net/vxlan/vxlan_vnifilter.c | 28 ++++++++++++++++++++++++++--
1 file changed, 26 insertions(+), 2 deletions(-)
diff --git a/drivers/net/vxlan/vxlan_vnifilter.c b/drivers/net/vxlan/vxlan_vnifilter.c
index 2b02f448c3e9..66a27198c65d 100644
--- a/drivers/net/vxlan/vxlan_vnifilter.c
+++ b/drivers/net/vxlan/vxlan_vnifilter.c
@@ -17,6 +17,11 @@
#include "vxlan_private.h"
+/* The most VNIs one dumped entry, or one dumped message, carries: the dump ends
+ * a coalesced run, and a message, before either would exceed it.
+ */
+#define VXLAN_VNI_FILTER_MSG_MAX 4096
+
static inline int vxlan_vni_cmp(struct rhashtable_compare_arg *arg,
const void *ptr)
{
@@ -342,6 +347,7 @@ static int vxlan_vnifilter_dump_dev(const struct net_device *dev,
struct nlmsghdr *nlh;
int idx = 0, s_idx;
bool dump_stats;
+ u32 vnis = 0;
int err = 0;
if (cb->args[2] != dev->ifindex) {
@@ -388,15 +394,29 @@ static int vxlan_vnifilter_dump_dev(const struct net_device *dev,
continue;
}
if (!dump_stats && vnirange(vend, v) == 1 &&
+ vnirange(vbegin, v) < VXLAN_VNI_FILTER_MSG_MAX &&
vxlan_addr_equal(&v->remote_ip, &vend->remote_ip)) {
goto update_end;
} else {
+ u32 sz = vnirange(vbegin, vend) + 1;
+
+ /* End this message on its VNI budget the same way we
+ * end it when the skb is full, so a dumped message
+ * never sums to more VNIs than the input path accepts
+ * in one request. Resume re-emits this run as the
+ * first entry of the next message.
+ */
+ if (vnis + sz > VXLAN_VNI_FILTER_MSG_MAX) {
+ err = -EMSGSIZE;
+ break;
+ }
if (!vxlan_fill_vni_filter_entry(skb, vbegin, vend,
dump_stats)) {
err = -EMSGSIZE;
break;
}
- idx += vnirange(vbegin, vend) + 1;
+ vnis += sz;
+ idx += sz;
vbegin = v;
}
update_end:
@@ -404,7 +424,11 @@ static int vxlan_vnifilter_dump_dev(const struct net_device *dev,
}
if (!err && vbegin) {
- if (!vxlan_fill_vni_filter_entry(skb, vbegin, vend, dump_stats))
+ if (vnis + vnirange(vbegin, vend) + 1 >
+ VXLAN_VNI_FILTER_MSG_MAX)
+ err = -EMSGSIZE;
+ else if (!vxlan_fill_vni_filter_entry(skb, vbegin, vend,
+ dump_stats))
err = -EMSGSIZE;
}
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH net-next v5 3/8] vxlan: vnifilter: bound the number of VNIs one request may touch
2026-11-14 22:14 [PATCH net-next v5 0/8] vxlan: vnifilter: bound a single request and account per-VNI memory Ali Firas
2026-11-14 22:14 ` [PATCH net-next v5 1/8] vxlan: vnifilter: reject VNIs outside the 24-bit space Ali Firas
2026-11-14 22:14 ` [PATCH net-next v5 2/8] vxlan: vnifilter: clamp the dumped VNI range to the request limit Ali Firas
@ 2026-11-14 22:14 ` Ali Firas
2026-11-14 22:14 ` [PATCH net-next v5 4/8] vxlan: vnifilter: account per-VNI memory to memcg Ali Firas
` (4 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Ali Firas @ 2026-11-14 22:14 UTC (permalink / raw)
To: netdev, idosch
Cc: kuba, pabeni, davem, edumazet, andrew+netdev, horms, razor,
roopa, kuniyu, petrm, aroulin, shuah, linux-kselftest,
linux-kernel, Ali Firas
A single RTM_NEWTUNNEL or RTM_DELTUNNEL message can ask for the whole
24-bit space in one range. vxlan_vni_add_del() then walks it one VNI at
a time under rtnl_lock, holding the lock for the length of the walk.
That walk is the cost being bounded, not the memory: an add allocates a
VNI node and a per-CPU stats block per VNI, a delete creates neither,
but both walk the same span and hold rtnl the same way, so both are
bounded.
The span of one VXLAN_VNIFILTER_ENTRY is not the quantity to bound. An
entry carrying just START and END is 20 bytes on the wire and a message
may carry many of them, vxlan_process_vni_filter() being called once per
entry, so bounding each entry alone would still let one message ask for
thousands of times the limit. Sum the spans of every entry and reject
the message as a whole in vxlan_vnifilter_check_msg(), before the
dispatch loop: entries are applied and notified one at a time, so a
limit enforced during dispatch would return an error only after every
preceding entry had been acted on. The range extraction is factored
into vxlan_vni_filter_entry_range() so the count and the range later
acted on cannot drift.
The limit, VXLAN_VNI_FILTER_MSG_MAX (4096), is the one "vxlan:
vnifilter: clamp the dumped VNI range to the request limit" introduced
for the dump, and its comment now names vxlan_vnifilter_check_msg() as
the input-side limit. It follows the usable VLAN ID space, since
vnifilter is mainly used on bridged devices where the VNI is derived
from the VLAN. It bounds one request, not how many VNIs a device may
hold: the whole space can still be installed in more than one request,
and a device holding it is still torn down in one step by
vxlan_vnigroup_uninit(), which is device teardown, not a netlink
message, and is not subject to this cap.
Found by code review of the same request path. In QEMU on net-next
(production build, 2 vCPU), one request for 1M VNIs took 1.6 s; an RTNL
request from another netns started 1 s in waited 0.6 s.
This is hardening for net-next -- a policy narrowing of what a single
message may ask for -- and is not a backport candidate.
Suggested-by: Ido Schimmel <idosch@nvidia.com>
Signed-off-by: Ali Firas <alishmery18@gmail.com>
---
v5: the VXLAN_VNI_FILTER_MSG_MAX comment names
vxlan_vnifilter_check_msg() as the input-side limit; the changelog names
the dump patch by subject, says this is net-next hardening, and says how
the walk was found and what one large request costs in QEMU (review of
v4, sinfo bot). v4: use the constant the dump patch introduces; rebased.
drivers/net/vxlan/vxlan_vnifilter.c | 86 +++++++++++++++++++++++++----
1 file changed, 76 insertions(+), 10 deletions(-)
diff --git a/drivers/net/vxlan/vxlan_vnifilter.c b/drivers/net/vxlan/vxlan_vnifilter.c
index 66a27198c65d..127ed5b2a2eb 100644
--- a/drivers/net/vxlan/vxlan_vnifilter.c
+++ b/drivers/net/vxlan/vxlan_vnifilter.c
@@ -17,8 +17,10 @@
#include "vxlan_private.h"
-/* The most VNIs one dumped entry, or one dumped message, carries: the dump ends
- * a coalesced run, and a message, before either would exceed it.
+/* The most VNIs one RTM_NEWTUNNEL/RTM_DELTUNNEL request may touch, summed over
+ * its entries, as enforced by vxlan_vnifilter_check_msg(). The dump ends a
+ * coalesced run, and a message, before either would exceed it, so no message it
+ * emits asks for more VNIs than one request may.
*/
#define VXLAN_VNI_FILTER_MSG_MAX 4096
@@ -970,13 +972,79 @@ static int vxlan_vni_add_del(struct vxlan_dev *vxlan, __u32 start_vni,
return err;
}
+/* Derive the VNI range one VXLAN_VNIFILTER_ENTRY selects. Shared so that the
+ * count taken by vxlan_vnifilter_check_msg() cannot drift from the range
+ * vxlan_process_vni_filter() then acts on.
+ */
+static void vxlan_vni_filter_entry_range(struct nlattr **vattrs, u32 *vni_start,
+ u32 *vni_end)
+{
+ *vni_start = 0;
+ *vni_end = 0;
+
+ if (vattrs[VXLAN_VNIFILTER_ENTRY_START]) {
+ *vni_start = nla_get_u32(vattrs[VXLAN_VNIFILTER_ENTRY_START]);
+ *vni_end = *vni_start;
+ }
+
+ if (vattrs[VXLAN_VNIFILTER_ENTRY_END])
+ *vni_end = nla_get_u32(vattrs[VXLAN_VNIFILTER_ENTRY_END]);
+}
+
+/* Reject a request touching more than VXLAN_VNI_FILTER_MSG_MAX VNIs before any
+ * of its entries is acted on. Both add and delete walk the span one VNI at a
+ * time under rtnl_lock, so both are bounded. Entries are applied one at a time
+ * and each one notifies as it goes, so a limit checked inside the dispatch loop
+ * would leave the entries ahead of the offending one already applied.
+ */
+static int vxlan_vnifilter_check_msg(const struct nlmsghdr *nlh,
+ struct netlink_ext_ack *extack)
+{
+ struct nlattr *vattrs[VXLAN_VNIFILTER_ENTRY_MAX + 1];
+ struct nlattr *attr;
+ u32 vnis = 0;
+ int err, rem;
+
+ nlmsg_for_each_attr_type(attr, VXLAN_VNIFILTER_ENTRY, nlh,
+ sizeof(struct tunnel_msg), rem) {
+ u32 vni_start, vni_end;
+
+ err = nla_parse_nested(vattrs, VXLAN_VNIFILTER_ENTRY_MAX, attr,
+ vni_filter_entry_policy, extack);
+ if (err)
+ return err;
+
+ vxlan_vni_filter_entry_range(vattrs, &vni_start, &vni_end);
+
+ /* A start above the end selects no VNI and costs nothing;
+ * leave it behaving as it does today.
+ */
+ if (vni_end < vni_start)
+ continue;
+
+ /* vni_filter_entry_policy has already bounded both endpoints
+ * below VXLAN_N_VID, so one entry adds at most VXLAN_N_VID and
+ * vnis cannot wrap before the test below rejects it.
+ */
+ vnis += vni_end - vni_start + 1;
+ if (vnis > VXLAN_VNI_FILTER_MSG_MAX) {
+ NL_SET_ERR_MSG_ATTR_FMT(extack, attr,
+ "Request asks for more than %u VNIs",
+ VXLAN_VNI_FILTER_MSG_MAX);
+ return -EINVAL;
+ }
+ }
+
+ return 0;
+}
+
static int vxlan_process_vni_filter(struct vxlan_dev *vxlan,
struct nlattr *nlvnifilter,
int cmd, struct netlink_ext_ack *extack)
{
struct nlattr *vattrs[VXLAN_VNIFILTER_ENTRY_MAX + 1];
const struct vxlan_config *cfg;
- u32 vni_start = 0, vni_end = 0;
+ u32 vni_start, vni_end;
union vxlan_addr group;
int err;
@@ -987,13 +1055,7 @@ static int vxlan_process_vni_filter(struct vxlan_dev *vxlan,
if (err)
return err;
- if (vattrs[VXLAN_VNIFILTER_ENTRY_START]) {
- vni_start = nla_get_u32(vattrs[VXLAN_VNIFILTER_ENTRY_START]);
- vni_end = vni_start;
- }
-
- if (vattrs[VXLAN_VNIFILTER_ENTRY_END])
- vni_end = nla_get_u32(vattrs[VXLAN_VNIFILTER_ENTRY_END]);
+ vxlan_vni_filter_entry_range(vattrs, &vni_start, &vni_end);
if (!vni_start && !vni_end) {
NL_SET_ERR_MSG_ATTR(extack, nlvnifilter,
@@ -1103,6 +1165,10 @@ static int vxlan_vnifilter_process(struct sk_buff *skb, struct nlmsghdr *nlh,
if (!(cfg->flags & VXLAN_F_VNIFILTER))
return -EOPNOTSUPP;
+ err = vxlan_vnifilter_check_msg(nlh, extack);
+ if (err)
+ return err;
+
nlmsg_for_each_attr_type(attr, VXLAN_VNIFILTER_ENTRY, nlh,
sizeof(*tmsg), rem) {
err = vxlan_process_vni_filter(vxlan, attr, nlh->nlmsg_type,
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH net-next v5 4/8] vxlan: vnifilter: account per-VNI memory to memcg
2026-11-14 22:14 [PATCH net-next v5 0/8] vxlan: vnifilter: bound a single request and account per-VNI memory Ali Firas
` (2 preceding siblings ...)
2026-11-14 22:14 ` [PATCH net-next v5 3/8] vxlan: vnifilter: bound the number of VNIs one request may touch Ali Firas
@ 2026-11-14 22:14 ` Ali Firas
2026-11-14 22:14 ` [PATCH net-next v5 5/8] vxlan: leave FDB state and flags unchanged when an update fails Ali Firas
` (3 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Ali Firas @ 2026-11-14 22:14 UTC (permalink / raw)
To: netdev, idosch
Cc: kuba, pabeni, davem, edumazet, andrew+netdev, horms, razor,
roopa, kuniyu, petrm, aroulin, shuah, linux-kselftest,
linux-kernel, Ali Firas
vxlan_vni_alloc() allocates a struct vxlan_vni_node and a per-CPU stats
block for every VNI, both with plain GFP_KERNEL, so neither is charged
to the cgroup of the process that created the VNI. A netdevice's own
struct and its Tx/Rx queues are already GFP_KERNEL_ACCOUNT in
alloc_netdev_mqs() (commit c948f51c1654 ("memcg: enable accounting for
net_device and Tx/Rx queues")), so these per-VNI allocations are an
unaccounted part of a device that a cgroup can grow one VNI at a time.
Charge both to the caller's cgroup with GFP_KERNEL_ACCOUNT in
vxlan_vni_alloc(). __GFP_ACCOUNT only charges an allocation made from a
task in a non-root memcg, so the sole caller, which already handles a
NULL return, is unaffected outside a memcg.
The charge lands in the caller's memcg: adding VNIs from inside one
moves each node into the accounted slab and raises the cgroup's per-CPU
memory. This covers the node and its stats only; a VNI carrying a group
or remote also installs a default FDB entry, whose allocations are not
charged here; "vxlan: vnifilter: account the default FDB entry to memcg"
later in this series accounts them.
Signed-off-by: Ali Firas <alishmery18@gmail.com>
---
v5: name the FDB accounting patch by subject; after the reordering it
is no longer the next patch.
v4: reworded to cover the node and stats only; the c948f51c1654
reference narrowed to net_device and its Tx/Rx queues; rebased.
drivers/net/vxlan/vxlan_vnifilter.c | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/drivers/net/vxlan/vxlan_vnifilter.c b/drivers/net/vxlan/vxlan_vnifilter.c
index 127ed5b2a2eb..07aa641f16e8 100644
--- a/drivers/net/vxlan/vxlan_vnifilter.c
+++ b/drivers/net/vxlan/vxlan_vnifilter.c
@@ -826,10 +826,11 @@ static struct vxlan_vni_node *vxlan_vni_alloc(struct vxlan_dev *vxlan,
{
struct vxlan_vni_node *vninode;
- vninode = kzalloc_obj(*vninode);
+ vninode = kzalloc_obj(*vninode, GFP_KERNEL_ACCOUNT);
if (!vninode)
return NULL;
- vninode->stats = netdev_alloc_pcpu_stats(struct vxlan_vni_stats_pcpu);
+ vninode->stats = __netdev_alloc_pcpu_stats(struct vxlan_vni_stats_pcpu,
+ GFP_KERNEL_ACCOUNT);
if (!vninode->stats) {
kfree(vninode);
return NULL;
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH net-next v5 5/8] vxlan: leave FDB state and flags unchanged when an update fails
2026-11-14 22:14 [PATCH net-next v5 0/8] vxlan: vnifilter: bound a single request and account per-VNI memory Ali Firas
` (3 preceding siblings ...)
2026-11-14 22:14 ` [PATCH net-next v5 4/8] vxlan: vnifilter: account per-VNI memory to memcg Ali Firas
@ 2026-11-14 22:14 ` Ali Firas
2026-11-14 22:14 ` [PATCH net-next v5 6/8] vxlan: vnifilter: roll back a VNI add that fails Ali Firas
` (2 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Ali Firas @ 2026-11-14 22:14 UTC (permalink / raw)
To: netdev, idosch
Cc: kuba, pabeni, davem, edumazet, andrew+netdev, horms, razor,
roopa, kuniyu, petrm, aroulin, shuah, linux-kselftest,
linux-kernel, Ali Firas
vxlan_fdb_update_existing() writes the requested state and flags into
the entry before the steps that can still fail: under NLM_F_REPLACE the
nexthop update and the check that refuses non-unicast entries, under
NLM_F_APPEND the allocation of the new remote, and the switchdev
notification. Each of them returns an error with the new state and
flags left in place, and no RTM_NEWNEIGH is sent for them, so the entry
changes while the request reports failure and nothing tells userspace.
For example, an RTM_NEWNEIGH asking to replace the permanent all-zeros
entry with a dynamic one is refused with -EOPNOTSUPP, yet leaves the
entry dynamic, and the ageing timer then deletes it.
Write the state and flags only after the nexthop update, the non-unicast
check and the allocation of the new remote, so that a request any of
them refuses never changes the entry, not even briefly. The switchdev
notification reports the new state, so it still follows the write; save
both fields on entry and put them back if a switchdev driver vetoes it.
A successful update ends in the same state and sends the same
notification as before. The ageing timer reads the state and flags
without the hash lock (see commit 7308ef8b4a67 ("vxlan: Stop rejecting
dynamic FDB entries with a nexthop ID")), so it can still age out an
entry that a concurrent successful update makes static; that race
predates this patch and is not addressed here.
The patch "vxlan: vnifilter: account the default FDB entry to memcg"
later in this series makes the NLM_F_APPEND allocation fail routinely
at a memory cgroup limit, on the path that changes a vnifilter VNI's
default entry; this is ordered before it so that failure leaves the
entry as it was. Like the rest of the series it targets net-next.
Found in the automated review of v4 of this series; the other error
exits were found by auditing the rest of the function. Each exit was
triggered in QEMU on net-next. The refused replace and the failed
nexthop update need only netlink requests; the failed append was
injected with the kernel's fail-nth fault injection, and the switchdev
veto was triggered with a test-only module that vetoes the notification,
as no switchdev hardware was available. In a production build (4 vCPU,
ageing 1 s), 356,026,806 refused requests in 1200 s, in equal parts
replaces of the permanent all-zeros entry refused with -EOPNOTSUPP,
appends to it failing on an injected -ENOMEM, and nexthop replaces of
another entry refused with -EINVAL, let the ageing timer delete the
all-zeros entry 1,193 times; the test re-added it after each deletion.
Tested in QEMU with the same requests: each exit now leaves the state
and flags as they were, and the loop above deleted the entry 0 times in
341,241,414 requests. test_vxlan_vnifiltering.sh, which gains a case
for the refused replace later in this series, passes with and without
KASAN and lockdep, and fib_nexthops.sh passes.
Fixes: 6681712d67ee ("vxlan: generalize forwarding tables")
Fixes: 906dc186e020 ("vxlan fdb replace an existing entry")
Fixes: 61f46fe8c646 ("vxlan: Allow vetoing of FDB notifications")
Fixes: 1274e1cc4226 ("vxlan: ecmp support for mac fdb entries")
Signed-off-by: Ali Firas <alishmery18@gmail.com>
---
v5: new patch (review of v4 6/7); says how the gap was found and what it
did in QEMU (sinfo bot); writes the state and flags after the steps that
can fail instead of writing and then reverting them, so a request those
steps refuse never changes them, not even briefly; a switchdev veto
still writes them and puts them back (local review and KCSAN runs).
drivers/net/vxlan/vxlan_core.c | 40 ++++++++++++++++++++--------------
1 file changed, 24 insertions(+), 16 deletions(-)
diff --git a/drivers/net/vxlan/vxlan_core.c b/drivers/net/vxlan/vxlan_core.c
index d11028b37472..2303d652d396 100644
--- a/drivers/net/vxlan/vxlan_core.c
+++ b/drivers/net/vxlan/vxlan_core.c
@@ -987,6 +987,8 @@ static int vxlan_fdb_update_existing(struct vxlan_dev *vxlan,
struct netlink_ext_ack *extack)
{
__u16 fdb_flags = (ndm_flags & ~NTF_USE);
+ __u16 old_flags = f->flags;
+ __u16 old_state = f->state;
struct vxlan_rdst *rd = NULL;
struct vxlan_rdst oldrd;
int notify = 0;
@@ -1005,21 +1007,6 @@ static int vxlan_fdb_update_existing(struct vxlan_dev *vxlan,
return -EOPNOTSUPP;
}
- /* Do not allow an externally learned entry to take over an entry added
- * by the user.
- */
- if (!(fdb_flags & NTF_EXT_LEARNED) ||
- !(f->flags & NTF_VXLAN_ADDED_BY_USER)) {
- if (f->state != state) {
- f->state = state;
- notify = 1;
- }
- if (f->flags != fdb_flags) {
- f->flags = fdb_flags;
- notify = 1;
- }
- }
-
if ((flags & NLM_F_REPLACE)) {
/* Only change unicasts */
if (!(is_multicast_ether_addr(f->key.eth_addr) ||
@@ -1048,6 +1035,23 @@ static int vxlan_fdb_update_existing(struct vxlan_dev *vxlan,
notify |= rc;
}
+ /* Do not allow an externally learned entry to take over an entry added
+ * by the user. The state and flags are written only after the steps
+ * above that can fail, so that a request they refuse never changes
+ * them, not even briefly: the ageing timer reads them without the lock.
+ */
+ if (!(fdb_flags & NTF_EXT_LEARNED) ||
+ !(f->flags & NTF_VXLAN_ADDED_BY_USER)) {
+ if (f->state != state) {
+ f->state = state;
+ notify = 1;
+ }
+ if (f->flags != fdb_flags) {
+ f->flags = fdb_flags;
+ notify = 1;
+ }
+ }
+
if (ndm_flags & NTF_USE)
WRITE_ONCE(f->updated, jiffies);
@@ -1066,13 +1070,17 @@ static int vxlan_fdb_update_existing(struct vxlan_dev *vxlan,
err_notify:
if (nhid)
- return err;
+ goto err_restore;
if ((flags & NLM_F_REPLACE) && rc)
*rd = oldrd;
else if ((flags & NLM_F_APPEND) && rc) {
list_del_rcu(&rd->list);
call_rcu(&rd->rcu, vxlan_dst_free);
}
+err_restore:
+ /* The notification was vetoed: put back the state and flags. */
+ f->state = old_state;
+ f->flags = old_flags;
return err;
}
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH net-next v5 6/8] vxlan: vnifilter: roll back a VNI add that fails
2026-11-14 22:14 [PATCH net-next v5 0/8] vxlan: vnifilter: bound a single request and account per-VNI memory Ali Firas
` (4 preceding siblings ...)
2026-11-14 22:14 ` [PATCH net-next v5 5/8] vxlan: leave FDB state and flags unchanged when an update fails Ali Firas
@ 2026-11-14 22:14 ` Ali Firas
2026-11-14 22:14 ` [PATCH net-next v5 7/8] vxlan: vnifilter: account the default FDB entry to memcg Ali Firas
2026-11-14 22:14 ` [PATCH net-next v5 8/8] selftests: net: test the vxlan vnifilter request limit and dump replay Ali Firas
7 siblings, 0 replies; 9+ messages in thread
From: Ali Firas @ 2026-11-14 22:14 UTC (permalink / raw)
To: netdev, idosch
Cc: kuba, pabeni, davem, edumazet, andrew+netdev, horms, razor,
roopa, kuniyu, petrm, aroulin, shuah, linux-kselftest,
linux-kernel, Ali Firas, Weiming Shi, Xiang Mei
vxlan_vni_add() publishes a new VNI before the steps that can still
fail: the node goes into the VNI rhashtable, the VNI list and, when the
device is up, the per-socket VNI hash. Only then does
vxlan_vni_update_group() install the VNI's default FDB entry and join
its multicast group. If either fails, nothing is undone and
RTM_NEWTUNNEL is sent anyway, so a request that returns an error leaves
behind a VNI that is listed, notified and accepts unicast VXLAN frames,
but has no default entry or no membership. A retry of a VNI that
inherits the device remote, or of one whose join failed, does not repair
it: vxlan_vni_add() then takes the update path, which finds the stored
remote equal to the requested one and returns 0.
Both failures are reachable without memory pressure. Installing the
entry fails with -ENOSPC once the device holds maxaddress FDB entries.
The join fails with -ENOBUFS once the socket holds
net.ipv4.igmp_max_memberships groups (20 by default).
Make installing the entry the last step that can fail, then undo the
publication when it does. On create, vxlan_vni_update_group() now joins
the group first and leaves it again if the entry cannot be installed.
The membership is given back only if this join took it, not when
-EADDRINUSE says the socket already held it. Installing the entry
either succeeds or leaves no FDB state behind: vxlan_fdb_create() frees
what it allocated, and "vxlan: leave FDB state and flags unchanged
when an update fails" covers an append to an existing entry. That leaves
vxlan_vni_add() only the VNI itself to unpublish. It does so with the
helpers vxlan_vni_del() uses, in reverse order of publication. It frees
the node through the same call_rcu(), so an RX or TX lookup that still
holds it under RCU does not see it freed. RTM_NEWTUNNEL is now sent
only on success. Commit aa6ca1c5c338 ("vxlan: vnifilter: send
notification on VNI add") sent it on failure too, because the VNI then
stayed installed; a VNI added without a group or remote still gets it,
as that commit intended.
vxlan_vni_delete_group() is not reused here. Its guard also fires when
only the device has a remote, so on such a device it deletes a default
entry even when installing that entry is what failed. Since
vxlan_fdb_parse() accepts any NDA_SRC_VNI, that entry can be one
installed out of band.
Ido raised the missing rollback in review of a vxlan_igmp_join() fix
(Links below), and the automated review of v4 of this series raised it
again. Both failures were triggered in QEMU on net-next with the limits
above, without modifying the kernel or injecting errors, and showed each
symptom described above.
Tested in QEMU: test_vxlan_vnifiltering.sh gains a case for each failure
later in this series; both fail on net-next and pass on this series,
with and without KASAN and lockdep.
Changing the group of an existing VNI is left as it is. It leaves the
old group before joining the new one, which lets a swap succeed at the
membership limit, so a join that fails there still has the new group
recorded. Fixing that needs a transaction.
This is ordered before "vxlan: vnifilter: account the default FDB entry
to memcg", which makes installing the entry fail routinely at a memory
cgroup limit. It targets net-next, as Jakub suggested in review of the
earlier posting.
Fixes: f9c4bb0b245c ("vxlan: vni filtering support on collect metadata device")
Link: https://lore.kernel.org/netdev/20260323095544.3311285-4-bestswngs@gmail.com/
Link: https://lore.kernel.org/netdev/20260324133449.GA460138@shredder/
Suggested-by: Ido Schimmel <idosch@nvidia.com>
Cc: Weiming Shi <bestswngs@gmail.com>
Cc: Xiang Mei <xmei5@asu.edu>
Signed-off-by: Ali Firas <alishmery18@gmail.com>
---
v5: new patch, ordered before the FDB accounting patch that makes the
failure routine (review of v4 6/7). The missing rollback was noted in
Weiming Shi's vxlan_igmp_join() fix and raised by Ido in its review
(Links); the changelog says what it did in QEMU (sinfo bot). It
supersedes the separately posted rollback series:
https://lore.kernel.org/netdev/20260911215203.3054653-1-alishmery18@gmail.com/
Unlike that posting it carries a Fixes tag, which maintainer-netdev.rst
requires for fixes regardless of the tree.
drivers/net/vxlan/vxlan_vnifilter.c | 50 +++++++++++++++++++++++------
1 file changed, 40 insertions(+), 10 deletions(-)
diff --git a/drivers/net/vxlan/vxlan_vnifilter.c b/drivers/net/vxlan/vxlan_vnifilter.c
index 07aa641f16e8..6e896ddf658c 100644
--- a/drivers/net/vxlan/vxlan_vnifilter.c
+++ b/drivers/net/vxlan/vxlan_vnifilter.c
@@ -606,6 +606,7 @@ static int vxlan_vni_update_group(struct vxlan_dev *vxlan,
const struct vxlan_config *cfg = rtnl_dereference(vxlan->cfg);
const union vxlan_addr *newrip = NULL, *oldrip = NULL;
union vxlan_addr old_remote_ip;
+ bool joined = false;
int ret = 0;
memcpy(&old_remote_ip, &vninode->remote_ip, sizeof(old_remote_ip));
@@ -633,13 +634,31 @@ static int vxlan_vni_update_group(struct vxlan_dev *vxlan,
if (!create && oldrip && newrip && vxlan_addr_equal(oldrip, newrip))
return 0;
+ /* A new VNI joins its group before its default FDB entry is installed,
+ * so that installing the entry is the last step that can fail, and a
+ * failure leaves vxlan_vni_add() only the VNI itself to undo.
+ */
+ if (create && group && (vxlan->dev->flags & IFF_UP) &&
+ vxlan_addr_multicast(group)) {
+ ret = vxlan_igmp_join(vxlan, group, 0);
+ if (ret == -EADDRINUSE)
+ ret = 0;
+ else if (!ret)
+ joined = true;
+ if (ret)
+ goto out;
+ }
+
ret = vxlan_update_default_fdb_entry(vxlan, vninode->vni,
oldrip, newrip,
cfg->remote_ifindex,
cfg->remote_ifindex,
extack);
- if (ret)
+ if (ret) {
+ if (joined)
+ vxlan_igmp_leave(vxlan, group, 0);
goto out;
+ }
if (group) {
memcpy(&vninode->remote_ip, group, sizeof(vninode->remote_ip));
@@ -647,7 +666,7 @@ static int vxlan_vni_update_group(struct vxlan_dev *vxlan,
vxlan_vnifilter_seq_inc(dev_net(vxlan->dev));
}
- if (vxlan->dev->flags & IFF_UP) {
+ if (!create && (vxlan->dev->flags & IFF_UP)) {
if (vxlan_addr_multicast(&old_remote_ip) &&
!vxlan_group_used(vn, vxlan, vninode->vni,
&old_remote_ip,
@@ -850,6 +869,14 @@ static void vxlan_vni_free(struct vxlan_vni_node *vninode)
kfree(vninode);
}
+static void vxlan_vni_node_rcu_free(struct rcu_head *rcu)
+{
+ struct vxlan_vni_node *v;
+
+ v = container_of(rcu, struct vxlan_vni_node, rcu);
+ vxlan_vni_free(v);
+}
+
static int vxlan_vni_add(struct vxlan_dev *vxlan,
struct vxlan_vni_group *vg,
u32 vni, union vxlan_addr *group,
@@ -889,18 +916,21 @@ static int vxlan_vni_add(struct vxlan_dev *vxlan,
err = vxlan_vni_update_group(vxlan, vninode, group, true, &changed,
extack);
+ if (err)
+ goto err_unpublish;
vxlan_vnifilter_notify(vxlan, vninode, RTM_NEWTUNNEL);
- return err;
-}
-
-static void vxlan_vni_node_rcu_free(struct rcu_head *rcu)
-{
- struct vxlan_vni_node *v;
+ return 0;
- v = container_of(rcu, struct vxlan_vni_node, rcu);
- vxlan_vni_free(v);
+err_unpublish:
+ if (vxlan->dev->flags & IFF_UP)
+ vxlan_vs_add_del_vninode(vxlan, vninode, true);
+ __vxlan_vni_del_list(vxlan, vg, vninode);
+ rhashtable_remove_fast(&vg->vni_hash, &vninode->vnode,
+ vxlan_vni_rht_params);
+ call_rcu(&vninode->rcu, vxlan_vni_node_rcu_free);
+ return err;
}
static int vxlan_vni_del(struct vxlan_dev *vxlan,
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH net-next v5 7/8] vxlan: vnifilter: account the default FDB entry to memcg
2026-11-14 22:14 [PATCH net-next v5 0/8] vxlan: vnifilter: bound a single request and account per-VNI memory Ali Firas
` (5 preceding siblings ...)
2026-11-14 22:14 ` [PATCH net-next v5 6/8] vxlan: vnifilter: roll back a VNI add that fails Ali Firas
@ 2026-11-14 22:14 ` Ali Firas
2026-11-14 22:14 ` [PATCH net-next v5 8/8] selftests: net: test the vxlan vnifilter request limit and dump replay Ali Firas
7 siblings, 0 replies; 9+ messages in thread
From: Ali Firas @ 2026-11-14 22:14 UTC (permalink / raw)
To: netdev, idosch
Cc: kuba, pabeni, davem, edumazet, andrew+netdev, horms, razor,
roopa, kuniyu, petrm, aroulin, shuah, linux-kselftest,
linux-kernel, Ali Firas
A VNI added through the vnifilter interface with a group or remote
installs a default FDB entry for that VNI: vxlan_vni_update_group()
calls vxlan_update_default_fdb_entry(), which calls vxlan_fdb_update()
and vxlan_fdb_create(). The entry allocates a struct vxlan_fdb and, per
remote, a struct vxlan_rdst plus its per-CPU dst cache, all with
GFP_ATOMIC and none charged to a memory cgroup.
"vxlan: vnifilter: account per-VNI memory to memcg" accounts the per-VNI
node and its stats; these FDB objects are the rest of what one vnifilter
request allocates per VNI. Charge them to the caller, as
commit c948f51c1654 ("memcg: enable accounting for net_device and Tx/Rx
queues") and commit e2c8d550a973 ("netfilter: ebtables: account
ebt_table_info to kmemcg") do: the lifetime of this memory is the net
namespace, not the allocating task, so the OOM killer cannot reclaim it
and it must be charged to the cgroup of the task whose request
allocates it.
Thread a gfp argument through vxlan_fdb_update() to
vxlan_fdb_alloc()/vxlan_fdb_append() on both paths it can take -- the
create path (vxlan_fdb_update_create() -> vxlan_fdb_create()) and the
path that changes an existing entry's remote
(vxlan_fdb_update_existing()) -- and a second gfp through
vxlan_update_default_fdb_entry(), so each caller chooses it. Every
non-vnifilter caller of vxlan_fdb_update() -- vxlan_fdb_add(),
vxlan_snoop(), vxlan_dev_create(), vxlan_changelink() and the switchdev
vxlan_fdb_external_learn_add() -- keeps plain GFP_ATOMIC, so the data
path and unrelated callers are unchanged. dst_cache_init() already takes
a gfp argument, so no change outside the driver is needed.
A per-request add or change of a VNI's remote (RTM_NEWTUNNEL, via
vxlan_vni_update_group()) passes GFP_NOWAIT | __GFP_ACCOUNT. These
allocations run under the hash spinlock and cannot sleep, but a
configuration path does not need the atomic reserves GFP_ATOMIC grants,
and GFP_NOWAIT makes the memcg charge hard-bounded rather than
force-charged: a cgroup at its limit fails the add or change there with
-ENOMEM instead of pushing memory.current past memory.max. That makes
the failure routine, which the two preceding patches prepare for:
"vxlan: vnifilter: roll back a VNI add that fails" unpublishes a VNI
whose create fails, and a failed change keeps the existing entry,
because the new remote is allocated before the old one is freed and
"vxlan: leave FDB state and flags unchanged when an update fails"
leaves the rest untouched. The per-CPU dst cache of a new remote is
charged too, but as before vxlan_fdb_append() does not treat its
allocation failing as fatal, so at the limit a remote can instead be
installed without one.
Measured on production builds in an 8 MiB cgroup at 2 and 8 vCPU: at the
limit a create or a remote change fails here with -ENOMEM, leaving the
VNI or its entry as it was, and nothing on this path was charged past
memory.max; at 2 vCPU some remotes were also installed without a dst
cache.
The device-wide default-remote change (RTM_NEWLINK, via
vxlan_vnilist_update_group()) passes GFP_ATOMIC | __GFP_ACCOUNT. Nothing
on that walk was charged before this patch; it is now charged to the
task issuing the RTM_NEWLINK, but force-charged, not hard-bounded. The
walk touches every VNI inheriting the device default in one message,
outside the per-request cap, and its error unwind re-installs entries
without checking whether that succeeded, so hard-bounding it would turn
a cgroup limit into a mid-walk failure the unwind cannot cleanly
reverse. Bounding that walk, and its unwind, are left to a separate
change. The vxlan_changelink() rollback of the device's own default
entry keeps plain GFP_ATOMIC, like the forward vxlan_fdb_update() call
it reverses.
This adds no new failure path on the data or learning paths: their
allocations already existed and keep GFP_ATOMIC; only the vnifilter
configuration path gains an -ENOMEM return, which its callers already
propagate.
Raised during the automated review of v3; Paolo Abeni agreed it required
fixing. In QEMU (production builds, 2 vCPU), 4000 inheriting VNIs charge
their cgroup 1101 B each before this patch, 1551 B with it.
Signed-off-by: Ali Firas <alishmery18@gmail.com>
---
v5: the vxlan_changelink() rollback keeps plain GFP_ATOMIC, like the
call it reverses; the changelog says the RTM_NEWLINK walk was not
charged before this patch, relies on the two new patches before it for a
clean failure instead of claiming one (review of v4), and says that at
the limit a remote can be installed without its dst cache; says what the
patch changes in QEMU (sinfo bot); re-measured; lines wrapped to 80
columns. v4: new patch.
drivers/net/vxlan/vxlan_core.c | 47 ++++++++++++++++-------------
drivers/net/vxlan/vxlan_private.h | 7 +++--
drivers/net/vxlan/vxlan_vnifilter.c | 14 ++++++---
3 files changed, 39 insertions(+), 29 deletions(-)
diff --git a/drivers/net/vxlan/vxlan_core.c b/drivers/net/vxlan/vxlan_core.c
index 2303d652d396..1f91d50accdc 100644
--- a/drivers/net/vxlan/vxlan_core.c
+++ b/drivers/net/vxlan/vxlan_core.c
@@ -577,7 +577,7 @@ static int vxlan_fdb_replace(struct vxlan_fdb *f,
/* Add/update destinations for multicast */
static int vxlan_fdb_append(struct vxlan_fdb *f,
union vxlan_addr *ip, __be16 port, __be32 vni,
- __u32 ifindex, struct vxlan_rdst **rdp)
+ __u32 ifindex, struct vxlan_rdst **rdp, gfp_t gfp)
{
struct vxlan_rdst *rd;
@@ -585,14 +585,14 @@ static int vxlan_fdb_append(struct vxlan_fdb *f,
if (rd)
return 0;
- rd = kmalloc_obj(*rd, GFP_ATOMIC);
+ rd = kmalloc_obj(*rd, gfp);
if (rd == NULL)
return -ENOMEM;
/* The driver can work correctly without a dst cache, so do not treat
* dst cache initialization errors as fatal.
*/
- dst_cache_init(&rd->dst_cache, GFP_ATOMIC | __GFP_NOWARN);
+ dst_cache_init(&rd->dst_cache, gfp | __GFP_NOWARN);
rd->remote_ip = *ip;
rd->remote_port = port;
@@ -779,11 +779,11 @@ static int vxlan_gpe_gro_complete(struct sock *sk, struct sk_buff *skb, int nhof
static struct vxlan_fdb *vxlan_fdb_alloc(struct vxlan_dev *vxlan, const u8 *mac,
__u16 state, __be32 src_vni,
- __u16 ndm_flags)
+ __u16 ndm_flags, gfp_t gfp)
{
struct vxlan_fdb *f;
- f = kmalloc_obj(*f, GFP_ATOMIC);
+ f = kmalloc_obj(*f, gfp);
if (!f)
return NULL;
memset(&f->key, 0, sizeof(f->key));
@@ -869,7 +869,7 @@ int vxlan_fdb_create(struct vxlan_dev *vxlan,
__u16 state, __be16 port, __be32 src_vni,
__be32 vni, __u32 ifindex, __u16 ndm_flags,
u32 nhid, struct vxlan_fdb **fdb,
- struct netlink_ext_ack *extack)
+ struct netlink_ext_ack *extack, gfp_t gfp)
{
const struct vxlan_config *cfg = rcu_dereference_rtnl(vxlan->cfg);
struct vxlan_rdst *rd = NULL;
@@ -881,14 +881,14 @@ int vxlan_fdb_create(struct vxlan_dev *vxlan,
return -ENOSPC;
netdev_dbg(vxlan->dev, "add %pM -> %pIS\n", mac, ip);
- f = vxlan_fdb_alloc(vxlan, mac, state, src_vni, ndm_flags);
+ f = vxlan_fdb_alloc(vxlan, mac, state, src_vni, ndm_flags, gfp);
if (!f)
return -ENOMEM;
if (nhid)
rc = vxlan_fdb_nh_update(vxlan, f, nhid, extack);
else
- rc = vxlan_fdb_append(f, ip, port, vni, ifindex, &rd);
+ rc = vxlan_fdb_append(f, ip, port, vni, ifindex, &rd, gfp);
if (rc < 0)
goto errout;
@@ -984,7 +984,7 @@ static int vxlan_fdb_update_existing(struct vxlan_dev *vxlan,
__u32 ifindex, __u16 ndm_flags,
struct vxlan_fdb *f, u32 nhid,
bool swdev_notify,
- struct netlink_ext_ack *extack)
+ struct netlink_ext_ack *extack, gfp_t gfp)
{
__u16 fdb_flags = (ndm_flags & ~NTF_USE);
__u16 old_flags = f->flags;
@@ -1028,7 +1028,8 @@ static int vxlan_fdb_update_existing(struct vxlan_dev *vxlan,
if ((flags & NLM_F_APPEND) &&
(is_multicast_ether_addr(f->key.eth_addr) ||
is_zero_ether_addr(f->key.eth_addr))) {
- rc = vxlan_fdb_append(f, ip, port, vni, ifindex, &rd);
+ rc = vxlan_fdb_append(f, ip, port, vni, ifindex, &rd,
+ gfp);
if (rc < 0)
return rc;
@@ -1090,7 +1091,7 @@ static int vxlan_fdb_update_create(struct vxlan_dev *vxlan,
__be16 port, __be32 src_vni, __be32 vni,
__u32 ifindex, __u16 ndm_flags, u32 nhid,
bool swdev_notify,
- struct netlink_ext_ack *extack)
+ struct netlink_ext_ack *extack, gfp_t gfp)
{
__u16 fdb_flags = (ndm_flags & ~NTF_USE);
struct vxlan_fdb *f;
@@ -1103,7 +1104,7 @@ static int vxlan_fdb_update_create(struct vxlan_dev *vxlan,
netdev_dbg(vxlan->dev, "add %pM -> %pIS\n", mac, ip);
rc = vxlan_fdb_create(vxlan, mac, ip, state, port, src_vni,
- vni, ifindex, fdb_flags, nhid, &f, extack);
+ vni, ifindex, fdb_flags, nhid, &f, extack, gfp);
if (rc < 0)
return rc;
@@ -1126,7 +1127,7 @@ int vxlan_fdb_update(struct vxlan_dev *vxlan,
__be16 port, __be32 src_vni, __be32 vni,
__u32 ifindex, __u16 ndm_flags, u32 nhid,
bool swdev_notify,
- struct netlink_ext_ack *extack)
+ struct netlink_ext_ack *extack, gfp_t gfp)
{
struct vxlan_fdb *f;
@@ -1140,7 +1141,8 @@ int vxlan_fdb_update(struct vxlan_dev *vxlan,
return vxlan_fdb_update_existing(vxlan, ip, state, flags, port,
vni, ifindex, ndm_flags, f,
- nhid, swdev_notify, extack);
+ nhid, swdev_notify, extack,
+ gfp);
} else {
if (!(flags & NLM_F_CREATE))
return -ENOENT;
@@ -1148,7 +1150,7 @@ int vxlan_fdb_update(struct vxlan_dev *vxlan,
return vxlan_fdb_update_create(vxlan, mac, ip, state, flags,
port, src_vni, vni, ifindex,
ndm_flags, nhid, swdev_notify,
- extack);
+ extack, gfp);
}
}
@@ -1284,7 +1286,7 @@ static int vxlan_fdb_add(struct ndmsg *ndm, struct nlattr *tb[],
err = vxlan_fdb_update(vxlan, addr, &ip, ndm->ndm_state, flags,
port, src_vni, vni, ifindex,
ndm->ndm_flags | NTF_VXLAN_ADDED_BY_USER,
- nhid, true, extack);
+ nhid, true, extack, GFP_ATOMIC);
spin_unlock_bh(&vxlan->hash_lock);
if (!err)
@@ -1504,7 +1506,8 @@ static enum skb_drop_reason vxlan_snoop(struct net_device *dev,
cfg->dst_port,
vni,
cfg->vni,
- ifindex, NTF_SELF, 0, true, NULL);
+ ifindex, NTF_SELF, 0, true, NULL,
+ GFP_ATOMIC);
spin_unlock(&vxlan->hash_lock);
}
@@ -4154,7 +4157,7 @@ static int vxlan_dev_create(struct net *net, struct net_device *dev,
cfg->vni,
cfg->vni,
cfg->remote_ifindex,
- NTF_SELF, 0, true, extack);
+ NTF_SELF, 0, true, extack, GFP_ATOMIC);
spin_unlock_bh(&vxlan->hash_lock);
if (err)
goto unlink;
@@ -4617,7 +4620,8 @@ static int vxlan_changelink(struct net_device *dev, struct nlattr *tb[],
cfg->dst_port,
conf.vni, conf.vni,
conf.remote_ifindex,
- NTF_SELF, 0, true, extack);
+ NTF_SELF, 0, true, extack,
+ GFP_ATOMIC);
if (err) {
spin_unlock_bh(&vxlan->hash_lock);
if (lowerdev_changed)
@@ -4652,7 +4656,8 @@ static int vxlan_changelink(struct net_device *dev, struct nlattr *tb[],
&cfg->remote_ip,
conf.remote_ifindex,
cfg->remote_ifindex,
- NULL);
+ NULL,
+ GFP_ATOMIC);
if (lowerdev_changed)
netdev_adjacent_change_abort(vxlan->lowerdev,
lowerdev, dev);
@@ -4972,7 +4977,7 @@ vxlan_fdb_external_learn_add(struct net_device *dev,
fdb_info->remote_vni,
fdb_info->remote_ifindex,
NTF_USE | NTF_SELF | NTF_EXT_LEARNED,
- 0, false, extack);
+ 0, false, extack, GFP_ATOMIC);
spin_unlock_bh(&vxlan->hash_lock);
return err;
diff --git a/drivers/net/vxlan/vxlan_private.h b/drivers/net/vxlan/vxlan_private.h
index f0ee85f83732..b5a80ed3eff2 100644
--- a/drivers/net/vxlan/vxlan_private.h
+++ b/drivers/net/vxlan/vxlan_private.h
@@ -192,7 +192,7 @@ int vxlan_fdb_create(struct vxlan_dev *vxlan,
__u16 state, __be16 port, __be32 src_vni,
__be32 vni, __u32 ifindex, __u16 ndm_flags,
u32 nhid, struct vxlan_fdb **fdb,
- struct netlink_ext_ack *extack);
+ struct netlink_ext_ack *extack, gfp_t gfp);
int __vxlan_fdb_delete(struct vxlan_dev *vxlan,
const unsigned char *addr, union vxlan_addr ip,
__be16 port, __be32 src_vni, __be32 vni,
@@ -202,7 +202,8 @@ int vxlan_fdb_update(struct vxlan_dev *vxlan,
__u16 state, __u16 flags,
__be16 port, __be32 src_vni, __be32 vni,
__u32 ifindex, __u16 ndm_flags, u32 nhid,
- bool swdev_notify, struct netlink_ext_ack *extack);
+ bool swdev_notify, struct netlink_ext_ack *extack,
+ gfp_t gfp);
void vxlan_xmit_one(struct sk_buff *skb, struct net_device *dev,
const struct vxlan_config *cfg,
__be32 default_vni, struct vxlan_rdst *rdst, bool did_rsc);
@@ -229,7 +230,7 @@ int vxlan_update_default_fdb_entry(struct vxlan_dev *vxlan, __be32 vni,
const union vxlan_addr *old_remote_ip,
const union vxlan_addr *remote_ip,
u32 old_ifindex, u32 new_ifindex,
- struct netlink_ext_ack *extack);
+ struct netlink_ext_ack *extack, gfp_t gfp);
int vxlan_vnilist_update_group(struct vxlan_dev *vxlan,
const union vxlan_addr *old_remote_ip,
const union vxlan_addr *new_remote_ip,
diff --git a/drivers/net/vxlan/vxlan_vnifilter.c b/drivers/net/vxlan/vxlan_vnifilter.c
index 6e896ddf658c..23e9b06e3c02 100644
--- a/drivers/net/vxlan/vxlan_vnifilter.c
+++ b/drivers/net/vxlan/vxlan_vnifilter.c
@@ -554,7 +554,7 @@ int vxlan_update_default_fdb_entry(struct vxlan_dev *vxlan, __be32 vni,
const union vxlan_addr *old_remote_ip,
const union vxlan_addr *remote_ip,
u32 old_ifindex, u32 new_ifindex,
- struct netlink_ext_ack *extack)
+ struct netlink_ext_ack *extack, gfp_t gfp)
{
const struct vxlan_config *cfg = rtnl_dereference(vxlan->cfg);
int err = 0;
@@ -576,7 +576,8 @@ int vxlan_update_default_fdb_entry(struct vxlan_dev *vxlan, __be32 vni,
vni,
vni,
new_ifindex,
- NTF_SELF, 0, true, extack);
+ NTF_SELF, 0, true, extack,
+ gfp);
if (err && extack) {
spin_unlock_bh(&vxlan->hash_lock);
return err;
@@ -653,7 +654,8 @@ static int vxlan_vni_update_group(struct vxlan_dev *vxlan,
oldrip, newrip,
cfg->remote_ifindex,
cfg->remote_ifindex,
- extack);
+ extack,
+ GFP_NOWAIT | __GFP_ACCOUNT);
if (ret) {
if (joined)
vxlan_igmp_leave(vxlan, group, 0);
@@ -727,7 +729,8 @@ int vxlan_vnilist_update_group(struct vxlan_dev *vxlan,
ret = vxlan_update_default_fdb_entry(vxlan, vent->vni,
oldrip, newrip,
old_ifindex, new_ifindex,
- extack);
+ extack, GFP_ATOMIC |
+ __GFP_ACCOUNT);
if (ret)
goto err_unwind;
}
@@ -749,7 +752,8 @@ int vxlan_vnilist_update_group(struct vxlan_dev *vxlan,
vxlan_update_default_fdb_entry(vxlan, vent->vni,
newrip, oldrip,
new_ifindex, old_ifindex,
- NULL);
+ NULL,
+ GFP_ATOMIC | __GFP_ACCOUNT);
}
return ret;
}
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH net-next v5 8/8] selftests: net: test the vxlan vnifilter request limit and dump replay
2026-11-14 22:14 [PATCH net-next v5 0/8] vxlan: vnifilter: bound a single request and account per-VNI memory Ali Firas
` (6 preceding siblings ...)
2026-11-14 22:14 ` [PATCH net-next v5 7/8] vxlan: vnifilter: account the default FDB entry to memcg Ali Firas
@ 2026-11-14 22:14 ` Ali Firas
7 siblings, 0 replies; 9+ messages in thread
From: Ali Firas @ 2026-11-14 22:14 UTC (permalink / raw)
To: netdev, idosch
Cc: kuba, pabeni, davem, edumazet, andrew+netdev, horms, razor,
roopa, kuniyu, petrm, aroulin, shuah, linux-kselftest,
linux-kernel, Ali Firas
Extend the vnifilter tests for the request-size limit, the 24-bit VNI
range, the dump/replay round trip the limit requires, the rollback of a
VNI add that fails, and an FDB update that is refused.
The API test gains: the largest accepted add and one VNI past it; a
rejected oversized add and a rejected multi-entry add whose entries are
each under the limit but sum over it, each checked to install nothing
(the range is asserted absent first, since vxlan_vni_add() folds an
existing VNI into the update path and would return 0 either way). The cap
is symmetric, so it also checks a maximum-size delete accepted and a
max+1 delete rejected, with every VNI in the max+1 range installed first
so the refusal can only come from the cap; a within-cap delete of a
never-installed range, which must fail on the missing VNI; and an
inverted range accepted as a no-op. The 24-bit bound is checked on each
endpoint alone: a lone VNI past the space is sent as START only and is
refused only by the START bound, and a two-VNI range from a valid START
is refused only by the END bound. The absence probes use vni_listed(),
which fails when "bridge vni show" itself fails, so a failed show cannot
pass for "absent".
vxlan_vnifilter_dump_replay() builds a contiguous run twice the limit
from two requests, each checked on its own, and requires the dump to
report exactly 10000-14095 and 14096-18191. It then replays every
reported range into a second device and requires all to be accepted and
the two dumps to match. bridge(8) prints entries, not netlink messages,
and no YNL spec covers RTM_GETTUNNEL, so this replays one entry per
request and cannot see the per-message budget of "vxlan: vnifilter:
clamp the dumped VNI range to the request limit"; that budget was
checked with raw netlink outside the tree.
vxlan_vnifilter_add_rollback() drives, without a memory cgroup, both
failures that "vxlan: vnifilter: roll back a VNI add that fails" undoes:
a VNI whose default FDB entry does not fit under maxaddress, and a VNI
whose group join would exceed net.ipv4.igmp_max_memberships. Each add
must fail and leave the VNI absent.
vxlan_vnifilter_refused_fdb_update() covers "vxlan: leave FDB state and
flags unchanged when an update fails". On a device with ageing 1, a
replace of a VNI's permanent default entry with a dynamic one must be
refused and leave the entry permanent: a dynamic entry added after it
is aged out first, which bounds the wait, and the default entry must
still be listed as permanent. Without that patch the refused replace
leaves the entry dynamic and the same ageing pass deletes it. The case
checks the end state only: a refused request that changed the state
and put it back before returning would pass it too, and only an ageing
pass running at that moment could tell the two apart.
vxlan_vnifilter_api() had no teardown of its own device and namespace;
add veth-host and the test netns to cleanup(), which runs on EXIT, so a
failing case does not leave them or its entries behind.
Suggested-by: Ido Schimmel <idosch@nvidia.com>
Signed-off-by: Ali Firas <alishmery18@gmail.com>
---
v5: absence probes fail when "bridge vni show" fails; each populate step
is logged; the dump is checked against the literal split; replay uses
its own result variable; START-only and END-only out-of-range cases
replace 100-4294967295, which the request cap refuses with or without
the END bound; new rollback cases (review of v4); a refused replace of a
VNI's permanent default entry must leave it permanent through an ageing
pass; lines wrapped to 80 columns. v4: assert the dump is non-empty
before replay, so the replay case cannot pass vacuously; rebased.
.../selftests/net/test_vxlan_vnifiltering.sh | 246 +++++++++++++++++-
1 file changed, 245 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/net/test_vxlan_vnifiltering.sh b/tools/testing/selftests/net/test_vxlan_vnifiltering.sh
index 6fb5af013dac..fb8401399b09 100755
--- a/tools/testing/selftests/net/test_vxlan_vnifiltering.sh
+++ b/tools/testing/selftests/net/test_vxlan_vnifiltering.sh
@@ -84,6 +84,9 @@ ret=0
# all tests in this script. Can be overridden with -t option
TESTS="
vxlan_vnifilter_api
+ vxlan_vnifilter_dump_replay
+ vxlan_vnifilter_add_rollback
+ vxlan_vnifilter_refused_fdb_update
vxlan_vnifilter_datapath
vxlan_vnifilter_datapath_pervni
vxlan_vnifilter_datapath_mgroup
@@ -139,8 +142,9 @@ check_vm_connectivity() {
cleanup() {
ip link del veth-hv-1 2>/dev/null || true
ip link del vethhv-11 vethhv-12 vethhv-21 vethhv-22 2>/dev/null || true
+ ip link del veth-host 2>/dev/null || true
- cleanup_ns $hv_1 $hv_2 $vm_11 $vm_21 $vm_12 $vm_22 $vm_31 $vm_32
+ cleanup_ns $hv_1 $hv_2 $vm_11 $vm_21 $vm_12 $vm_22 $vm_31 $vm_32 $testns
}
trap cleanup EXIT
@@ -285,6 +289,37 @@ cleanup_vnifilter_api()
ip netns del $testns 2>/dev/null || true
}
+# Succeed if "bridge vni show" lists VNI $2 on device $1. A failed show
+# returns 2, so a probe expecting the VNI to be absent cannot pass on it.
+vni_listed()
+{
+ local out
+
+ out=$(bridge -netns $testns vni show dev $1) || return 2
+ grep -qw "$2" <<< "$out"
+}
+
+# Print the VNI ranges "bridge vni show" reports for device $1, sorted;
+# fail if the show itself fails.
+vni_ranges()
+{
+ local out
+
+ out=$(bridge -netns $testns vni show dev $1) || return 1
+ grep -oE '[0-9]+-[0-9]+|[0-9]{2,}' <<< "$out" | sort
+}
+
+# Succeed if "bridge fdb show" lists, on device $1, an entry whose line
+# matches the extended regex $2. A failed show returns 2, so a probe
+# expecting the entry to be absent cannot pass on it.
+fdb_listed()
+{
+ local out
+
+ out=$(bridge -netns $testns fdb show dev $1) || return 2
+ grep -qE "$2" <<< "$out"
+}
+
# tests vxlan filtering api
vxlan_vnifilter_api()
{
@@ -347,6 +382,215 @@ vxlan_vnifilter_api()
# change vxlan vnifilter flag
run_cmd "ip -netns $testns link set dev vxlan-ext1 type vxlan external novnifilter"
log_test $? 2 "Cannot unset vnifilter flag on a device"
+
+ # A single request may add at most 4096 VNIs. bridge(8) puts one
+ # VXLAN_VNIFILTER_ENTRY per comma-separated item into a single message,
+ # so both the single-entry and the multi-entry paths are reachable here.
+
+ # The largest accepted add, and one VNI more rejected.
+ run_cmd "bridge -netns $testns vni add dev vxlan-ext1 vni 10000-14095"
+ log_test $? 0 "Add a request of the maximum VNI count"
+
+ # The rejected oversized add must install nothing. Assert the range is
+ # absent first: vxlan_vni_add() folds an already-present VNI into the
+ # update path and returns 0, so a later probe cannot tell "installed
+ # nothing" from "installed part" unless it started absent.
+ run_cmd "vni_listed vxlan-ext1 20000"
+ log_test $? 1 "VNI 20000 absent before the oversized add"
+ run_cmd "bridge -netns $testns vni add dev vxlan-ext1 vni 20000-24096"
+ log_test $? 255 "Cannot add a request over the maximum VNI count"
+ run_cmd "vni_listed vxlan-ext1 20000"
+ log_test $? 1 "The rejected oversized add installed nothing"
+
+ # Two entries each under the limit but summing over it: the per-message
+ # total is what is bounded, not the span of one entry. This is the shape
+ # a per-entry check would have let through.
+ run_cmd "vni_listed vxlan-ext1 30000"
+ log_test $? 1 "VNI 30000 absent before the oversized multi-entry add"
+ run_cmd "bridge -netns $testns vni add dev vxlan-ext1 \
+ vni 30000-32047,32048-34097"
+ log_test $? 255 "Cannot add multiple entries summing over the maximum"
+ run_cmd "vni_listed vxlan-ext1 30000"
+ log_test $? 1 "The rejected multi-entry add installed nothing"
+
+ # The cap is symmetric: a delete may touch at most the maximum too.
+ # Install a maximum-size range and the VNI past it, so the max+1 delete
+ # below has every VNI present and can only be refused by the cap, not by
+ # a missing VNI.
+ run_cmd "bridge -netns $testns vni add dev vxlan-ext1 vni 40000-44095"
+ log_test $? 0 "Populate a maximum-size range to delete"
+ run_cmd "bridge -netns $testns vni add dev vxlan-ext1 vni 44096"
+ log_test $? 0 "Add the VNI past the maximum range"
+ run_cmd "bridge -netns $testns vni del dev vxlan-ext1 vni 40000-44096"
+ log_test $? 255 "Cannot delete a request over the maximum VNI count"
+ run_cmd "bridge -netns $testns vni del dev vxlan-ext1 vni 40000-44095"
+ log_test $? 0 "Delete a request of the maximum VNI count"
+ run_cmd "bridge -netns $testns vni del dev vxlan-ext1 vni 44096"
+ log_test $? 0 "Delete the VNI past the maximum range"
+
+ # A within-cap delete of a never-installed range reaches the handler and
+ # fails there on the missing VNI, not on the cap.
+ run_cmd "bridge -netns $testns vni del dev vxlan-ext1 vni 50000-50010"
+ log_test $? 255 "Cannot delete a range that was never installed"
+
+ # A start above the end selects nothing and is accepted as a no-op.
+ # Use a VNI no earlier case installs, so the absence check is
+ # meaningful.
+ run_cmd "vni_listed vxlan-ext1 55000"
+ log_test $? 1 "VNI 55000 absent before the inverted range"
+ run_cmd "bridge -netns $testns vni add dev vxlan-ext1 vni 55000-50000"
+ log_test $? 0 "An inverted range is accepted as a no-op"
+ run_cmd "vni_listed vxlan-ext1 55000"
+ log_test $? 1 "The inverted range installed nothing"
+
+ # The VXLAN header carries 24 bits, and both endpoints are bounded. A
+ # lone VNI is sent as START only and spans one VNI, so only the START
+ # bound refuses it; a range from a valid START spans two VNIs, so only
+ # the END bound refuses it. Neither is large enough for the request
+ # limit to catch.
+ run_cmd "bridge -netns $testns vni add dev vxlan-ext1 vni 16777215"
+ log_test $? 0 "Add the highest VNI the header can carry"
+ run_cmd "bridge -netns $testns vni del dev vxlan-ext1 vni 16777215"
+ log_test $? 0 "Delete the highest VNI the header can carry"
+ run_cmd "bridge -netns $testns vni add dev vxlan-ext1 vni 16777216"
+ log_test $? 255 "Cannot add a VNI past the 24-bit space"
+ run_cmd "bridge -netns $testns vni add dev vxlan-ext1 \
+ vni 16777215-16777216"
+ log_test $? 255 "Cannot add a range whose END leaves the 24-bit space"
+}
+
+# A device may hold a contiguous run longer than one request's limit, built
+# from several requests. The dump coalesces it, so the dump must break the run
+# into entries each no larger than the limit, or the configuration it reports
+# cannot be replayed. Install such a run, then feed every range the dump
+# reports back into a second device and require all to be accepted.
+vxlan_vnifilter_dump_replay()
+{
+ local opts="external vnifilter local 172.16.0.1 dev veth-testns"
+ local range replay_rc src_ranges dst_ranges
+
+ cleanup_vnifilter_api &>/dev/null
+ setup_vnifilter_api
+
+ # The destination is on a different dstport so re-adding the same VNIs
+ # does not collide with the source under vxlan_vni_in_use().
+ run_cmd "ip -netns $testns link add vxlan-src type vxlan $opts \
+ dstport 4789"
+ log_test $? 0 "dump/replay: create source device"
+ run_cmd "ip -netns $testns link add vxlan-dst type vxlan $opts \
+ dstport 4790"
+ log_test $? 0 "dump/replay: create destination device"
+
+ # 8192 contiguous VNIs sharing the default remote: one run, twice the
+ # limit, installed in two accepted requests.
+ run_cmd "bridge -netns $testns vni add dev vxlan-src vni 10000-14095"
+ log_test $? 0 "dump/replay: populate the first half of the run"
+ run_cmd "bridge -netns $testns vni add dev vxlan-src vni 14096-18191"
+ log_test $? 0 "dump/replay: populate the second half of the run"
+
+ # The run must come back split at the limit, exactly. Comparing with
+ # the literal split, not only with the replayed copy, also catches a
+ # dump that drops or repeats VNIs where it resumes.
+ src_ranges=$(vni_ranges vxlan-src)
+ log_test $? 0 "dump/replay: dump the source device"
+ [ "$src_ranges" = "$(printf '10000-14095\n14096-18191')" ]
+ log_test $? 0 "dump/replay: the run is dumped split at the limit"
+
+ # bridge(8) prints entries, not netlink messages, so this replays one
+ # entry per request; it does not see the per-message budget.
+ replay_rc=0
+ for range in $src_ranges; do
+ bridge -netns $testns vni add dev vxlan-dst vni "$range" \
+ 2>/dev/null || replay_rc=$?
+ done
+ log_test $replay_rc 0 "dump/replay: replay accepts every dumped entry"
+
+ dst_ranges=$(vni_ranges vxlan-dst)
+ log_test $? 0 "dump/replay: dump the destination device"
+ [ "$src_ranges" = "$dst_ranges" ]
+ log_test $? 0 "dump/replay: source and destination dump the same ranges"
+}
+
+# A VNI whose add fails must not be left installed. Two steps can fail
+# after the VNI is published, and both are reachable without memory
+# pressure: installing its default FDB entry, once the device holds
+# maxaddress entries, and joining its group, once the socket holds
+# net.ipv4.igmp_max_memberships groups.
+vxlan_vnifilter_add_rollback()
+{
+ local opts="external vnifilter local 172.16.0.1 dev veth-testns"
+ local full="dstport 4789 remote 172.16.0.2 maxaddress 1"
+ local groups grp vni k fail=0
+
+ cleanup_vnifilter_api &>/dev/null
+ setup_vnifilter_api
+
+ # The device's own default entry uses up maxaddress 1, so no VNI that
+ # inherits the device remote can install its own.
+ run_cmd "ip -netns $testns link add vxlan-full type vxlan $opts $full"
+ log_test $? 0 "rollback: create a device with room for one FDB entry"
+ run_cmd "ip -netns $testns link set dev vxlan-full up"
+ run_cmd "bridge -netns $testns vni add dev vxlan-full vni 100"
+ log_test $? 255 "rollback: refuse a VNI whose FDB entry does not fit"
+ run_cmd "vni_listed vxlan-full 100"
+ log_test $? 1 "rollback: the VNI whose FDB entry failed is absent"
+
+ groups=$(ip netns exec $testns sysctl -n net.ipv4.igmp_max_memberships)
+ run_cmd "ip -netns $testns link add vxlan-mc type vxlan $opts \
+ dstport 4790"
+ log_test $? 0 "rollback: create a device for multicast groups"
+ run_cmd "ip -netns $testns link set dev vxlan-mc up"
+ # VNIs above 255 so that no group address can match the VNI probe.
+ for k in $(seq 1 $groups); do
+ grp=239.1.$((k / 256)).$((k % 256))
+ bridge -netns $testns vni add dev vxlan-mc vni $((1000 + k)) \
+ group $grp 2>/dev/null || fail=1
+ done
+ log_test $fail 0 "rollback: join as many groups as the socket allows"
+ vni=$((1001 + groups))
+ run_cmd "bridge -netns $testns vni add dev vxlan-mc vni $vni \
+ group 239.2.0.1"
+ log_test $? 255 "rollback: refuse a VNI whose group cannot be joined"
+ run_cmd "vni_listed vxlan-mc $vni"
+ log_test $? 1 "rollback: the VNI whose join failed is not installed"
+}
+
+# A refused FDB update must leave the entry's state as it was. Replacing a
+# VNI's permanent default (all-zeros) entry with a dynamic one is refused,
+# because only unicast entries can be replaced, so the entry must stay
+# permanent and survive ageing. A dynamic entry added after the refused
+# request is due for ageing no earlier than the default entry, so once it
+# has been aged out the default entry has been through the same pass. The
+# check is on the end state only: a request that changed the state and
+# put it back before returning would pass it too.
+vxlan_vnifilter_refused_fdb_update()
+{
+ local opts="external vnifilter local 172.16.0.1 dev veth-testns"
+ local zero="^00:00:00:00:00:00 dst 172.16.0.2 .*src_vni 100 .*self"
+ local mac=02:00:00:00:00:01
+
+ cleanup_vnifilter_api &>/dev/null
+ setup_vnifilter_api
+
+ run_cmd "ip -netns $testns link add vxlan-age type vxlan $opts \
+ dstport 4789 ageing 1"
+ log_test $? 0 "refused update: create a device with ageing 1"
+ run_cmd "ip -netns $testns link set dev vxlan-age up"
+ run_cmd "bridge -netns $testns vni add dev vxlan-age vni 100 \
+ remote 172.16.0.2"
+ log_test $? 0 "refused update: add a VNI with a remote"
+ run_cmd "fdb_listed vxlan-age '$zero permanent'"
+ log_test $? 0 "refused update: the VNI's default entry is permanent"
+ run_cmd "bridge -netns $testns fdb replace 00:00:00:00:00:00 \
+ dev vxlan-age dst 172.16.0.3 src_vni 100 self dynamic"
+ log_test $? 255 "refused update: cannot replace the default entry"
+ run_cmd "bridge -netns $testns fdb add $mac dev vxlan-age \
+ dst 172.16.0.4 src_vni 100 self dynamic"
+ log_test $? 0 "refused update: add a dynamic entry"
+ slowwait 25 eval "! fdb_listed vxlan-age '^$mac '"
+ log_test $? 0 "refused update: the dynamic entry is aged out"
+ run_cmd "fdb_listed vxlan-age '$zero permanent'"
+ log_test $? 0 "refused update: the default entry is still permanent"
}
# Sanity test vnifilter datapath
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread