From 1f3b8c70b2249d76d0d1bf0c809a92147b46b93e Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Fri, 13 Feb 2026 16:44:18 +0100 Subject: [PATCH 1/6] smoke: fix too permissive grep expressions grep -q '10.' can match any 3 characters that start with '10' (e.g. 100, 10a, 10:, etc.). So it can match parts of IPv6 link local addresses. Use full address and -F/--fixed-strings to avoid any special regexp characters. We want verbatim match. Fixes: 74228b710d00 ("cli: add address flush command") Signed-off-by: Robin Jarry --- smoke/config_test.sh | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/smoke/config_test.sh b/smoke/config_test.sh index 986b549fc..d9466b7d4 100755 --- a/smoke/config_test.sh +++ b/smoke/config_test.sh @@ -51,25 +51,27 @@ grcli stats show software grcli stats show hardware # Test address del cleans up connected routes grcli address del 10.1.0.1/24 iface p3 -grcli route show | grep -q '10.1.0.0/24' && fail "connected route should be removed after address del" +grcli route show | grep -qF '10.1.0.0/24' && fail "connected route should be removed after address del" grcli address del 2346::1/24 iface p3 -grcli route show | grep -q '2346::/24' && fail "connected route should be removed after address del" +grcli route show | grep -qF '2346::/24' && fail "connected route should be removed after address del" # Test address flush with family filter grcli address add 10.2.0.1/24 iface p3 grcli address add 2347::1/24 iface p3 +grcli address show iface p3 grcli address flush ip4 iface p3 -grcli address show iface p3 | grep -q '10.' && fail "p3 should have no IPv4 after ip4 flush" -grcli address show iface p3 | grep -q '2347::' || fail "p3 should still have IPv6 after ip4 flush" +grcli address show iface p3 +grcli address show iface p3 | grep -qF '10.2.0.1/24' && fail "p3 should have no IPv4 after ip4 flush" +grcli address show iface p3 | grep -qF '2347::1/24' || fail "p3 should still have IPv6 after ip4 flush" grcli address flush ip6 iface p3 -grcli address show iface p3 | grep -q '2347::' && fail "p3 should have no user IPv6 after ip6 flush" -grcli address show iface p3 | grep -q 'fe80::' || fail "p3 should still have link-local after ip6 flush" +grcli address show iface p3 | grep -qF '2347::1/24' && fail "p3 should have no user IPv6 after ip6 flush" +grcli address show iface p3 | grep -qF 'fe80::' || fail "p3 should still have link-local after ip6 flush" # Test flush all families (default) grcli address add 10.3.0.1/24 iface p3 grcli address add 2348::1/24 iface p3 grcli address flush iface p3 -grcli address show iface p3 | grep -q '10.' && fail "p3 should have no IPv4 after flush" -grcli address show iface p3 | grep -q '2348::' && fail "p3 should have no user IPv6 after flush" -grcli address show iface p3 | grep -q 'fe80::' || fail "p3 should still have link-local after flush" +grcli address show iface p3 | grep -qF '10.3.0.1/24' && fail "p3 should have no IPv4 after flush" +grcli address show iface p3 | grep -qF '2348::1/24' && fail "p3 should have no user IPv6 after flush" +grcli address show iface p3 | grep -qF 'fe80::' || fail "p3 should still have link-local after flush" grcli nexthop del 42 grcli nexthop del 666 From 9be621af582d8829a527b812c3139cbe623a76df Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Fri, 13 Feb 2026 16:36:30 +0100 Subject: [PATCH 2/6] smoke: fix typo in vrf config test There is no "name" argument available when creating an interface. The name is the first argument. Fixes: 9d5152f8d434 ("smoke: add VRF configuration tests") Signed-off-by: Robin Jarry --- smoke/config_vrf_test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smoke/config_vrf_test.sh b/smoke/config_vrf_test.sh index 747897073..dbc6fba43 100755 --- a/smoke/config_vrf_test.sh +++ b/smoke/config_vrf_test.sh @@ -12,7 +12,7 @@ grcli interface del p4 grcli interface del testvrf # reserved name rejection -grcli interface add port p4 devargs net_null2,no-rx=1 name gr-loop99 \ +grcli interface add port gr-loop99 devargs net_null2,no-rx=1 \ && fail "reserved name gr-loop99 should be rejected" grcli interface add vrf gr-loop99 \ && fail "reserved name gr-loop99 should be rejected for VRF" From 475618ee14be536f242bfcb192b94baecfbd8e4a Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 11 Feb 2026 14:11:46 +0100 Subject: [PATCH 3/6] xconnect: fix segfault when deleting a port peer When removing a port which is the xconnect peer of another one, iface_from_id(iface->domain_id) will return NULL since the interface was deleted. Program terminated with signal SIGSEGV, Segmentation fault. xconnect_process at modules/infra/datapath/xconnect.c:36 if (peer->type == GR_IFACE_TYPE_PORT) { __rte_node_process at subprojects/dpdk/lib/graph/rte_graph_worker_common.h:216 rte_graph_walk_rtc at subprojects/dpdk/lib/graph/rte_graph_model_rtc.h:42 rte_graph_walk at subprojects/dpdk/lib/graph/rte_graph_worker.h:38 gr_datapath_loop at modules/infra/datapath/main_loop.c:252 Check the return value and drop the packet in that case. Signed-off-by: Robin Jarry --- modules/infra/datapath/xconnect.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/infra/datapath/xconnect.c b/modules/infra/datapath/xconnect.c index 37e4daf1b..4f2a97b33 100644 --- a/modules/infra/datapath/xconnect.c +++ b/modules/infra/datapath/xconnect.c @@ -34,7 +34,7 @@ xconnect_process(struct rte_graph *graph, struct rte_node *node, void **objs, ui IFACE_STATS_INC(rx, mbuf, iface); - if (peer->type == GR_IFACE_TYPE_PORT) { + if (peer != NULL && peer->type == GR_IFACE_TYPE_PORT) { port = iface_info_port(peer); mbuf->port = port->port_id; edge = OUTPUT; From 58b1fce0ef28fe80789e8860e5b82163c231f474 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Tue, 10 Feb 2026 13:29:03 +0100 Subject: [PATCH 4/6] ip,ip6: flush addresses on interface mode change When an interface leaves VRF mode (e.g. reconfigured as cross-connect), any IPv4 and IPv6 addresses previously configured on it become invalid. Likewise, when an interface moves to a different VRF, its addresses belong to the old VRF and need to be removed. Subscribe to GR_EVENT_IFACE_POST_RECONFIG in both IPv4 and IPv6 address modules. On reconfiguration, flush all addresses when the interface is no longer in VRF mode or has moved to a different VRF. For IPv6, also reinitialize link-local and well-known multicast addresses when entering VRF mode or changing VRFs. Extend the IPv6 add/del smoke test to exercise VRF reassignment and cross-connect mode transitions. Signed-off-by: Robin Jarry --- modules/ip/control/address.c | 17 +++++--- modules/ip6/control/address.c | 76 +++++++++++++++++++++++------------ smoke/ip6_add_del_test.sh | 11 +++++ 3 files changed, 74 insertions(+), 30 deletions(-) diff --git a/modules/ip/control/address.c b/modules/ip/control/address.c index 6c970a061..3649aabe3 100644 --- a/modules/ip/control/address.c +++ b/modules/ip/control/address.c @@ -257,15 +257,22 @@ static struct api_out addr_list(const void *request, struct api_ctx *ctx) { return api_out(0, 0, NULL); } -static void iface_pre_remove_cb(uint32_t /*event*/, const void *obj) { +static void iface_event_cb(uint32_t event, const void *obj) { const struct iface *iface = obj; struct nexthop_info_l3 *l3; struct hoplist *ifaddrs; ifaddrs = addr4_get_all(iface->id); - if (ifaddrs == NULL) + if (ifaddrs == NULL || gr_vec_len(ifaddrs->nh) == 0) return; + if (event == GR_EVENT_IFACE_POST_RECONFIG) { + if (iface->mode == GR_IFACE_MODE_VRF && iface->vrf_id == ifaddrs->nh[0]->vrf_id) + return; + } + + // interface is either being deleted, mode changed to !VRF, or changed to another VRF + // delete all configured addresses while (gr_vec_len(ifaddrs->nh) > 0) { l3 = nexthop_info_l3(ifaddrs->nh[gr_vec_len(ifaddrs->nh) - 1]); addr4_delete(iface->id, l3->ipv4, l3->prefixlen); @@ -323,9 +330,9 @@ static struct gr_module addr_module = { }; static struct gr_event_subscription iface_pre_rm_subscription = { - .callback = iface_pre_remove_cb, - .ev_count = 1, - .ev_types = {GR_EVENT_IFACE_PRE_REMOVE}, + .callback = iface_event_cb, + .ev_count = 2, + .ev_types = {GR_EVENT_IFACE_POST_RECONFIG, GR_EVENT_IFACE_PRE_REMOVE}, }; static struct gr_event_subscription iface_up_subscription = { .callback = iface_up_cb, diff --git a/modules/ip6/control/address.c b/modules/ip6/control/address.c index 31d2de8aa..ab7c25801 100644 --- a/modules/ip6/control/address.c +++ b/modules/ip6/control/address.c @@ -417,40 +417,65 @@ static const struct rte_ipv6_addr well_known_mcast_addrs[] = { GR_IPV6_ADDR_MLDV2, }; -static void ip6_iface_event_handler(uint32_t event, const void *obj) { - const struct nexthop_info_l3 *l3; - const struct iface *iface = obj; +static void ip6_iface_llocal_init(const struct iface *iface) { struct rte_ipv6_addr link_local; struct rte_ether_addr mac; + unsigned i; + + if (iface_get_eth_addr(iface, &mac) == 0) { + rte_ipv6_llocal_from_ethernet(&link_local, &mac); + if (iface6_addr_add(iface, &link_local, 64) < 0) + errno_log(errno, "iface_addr_add"); + } + for (i = 0; i < ARRAY_DIM(well_known_mcast_addrs); i++) { + if (mcast6_addr_add(iface, &well_known_mcast_addrs[i]) < 0) + LOG(INFO, "%s: mcast_addr_add: %s", iface->name, strerror(errno)); + } +} + +static void ip6_iface_addrs_flush(const struct iface *iface) { + const struct nexthop_info_l3 *l3; + const struct nexthop *nh; + struct hoplist *addrs; + + addrs = &iface_addrs[iface->id]; + while (gr_vec_len(addrs->nh) > 0) { + nh = addrs->nh[gr_vec_len(addrs->nh) - 1]; + l3 = nexthop_info_l3(nh); + iface6_addr_del(iface, &l3->ipv6, l3->prefixlen); + } + addrs = &iface_mcast_addrs[iface->id]; + while (gr_vec_len(addrs->nh) > 0) { + nh = addrs->nh[gr_vec_len(addrs->nh) - 1]; + l3 = nexthop_info_l3(nh); + mcast6_addr_del(iface, &l3->ipv6); + } +} + +static void ip6_iface_event_handler(uint32_t event, const void *obj) { + const struct iface *iface = obj; const struct nexthop *nh; struct hoplist *addrs; - unsigned i; switch (event) { case GR_EVENT_IFACE_POST_ADD: - if (iface_get_eth_addr(iface, &mac) == 0) { - rte_ipv6_llocal_from_ethernet(&link_local, &mac); - if (iface6_addr_add(iface, &link_local, 64) < 0) - errno_log(errno, "iface_addr_add"); - } - for (i = 0; i < ARRAY_DIM(well_known_mcast_addrs); i++) { - if (mcast6_addr_add(iface, &well_known_mcast_addrs[i]) < 0) - LOG(INFO, "%s: mcast_addr_add: %s", iface->name, strerror(errno)); + ip6_iface_llocal_init(iface); + break; + case GR_EVENT_IFACE_POST_RECONFIG: + if (iface->mode != GR_IFACE_MODE_VRF) { + // changing mode from VRF -> !VRF + ip6_iface_addrs_flush(iface); + } else if (gr_vec_len(iface_addrs[iface->id].nh) == 0) { + // changing mode from !VRF -> VRF + ip6_iface_llocal_init(iface); + } else if (iface_addrs[iface->id].nh[0]->vrf_id != iface->vrf_id) { + // changing to a different VRF + ip6_iface_addrs_flush(iface); + ip6_iface_llocal_init(iface); } break; case GR_EVENT_IFACE_PRE_REMOVE: - addrs = &iface_addrs[iface->id]; - while (gr_vec_len(addrs->nh) > 0) { - nh = addrs->nh[gr_vec_len(addrs->nh) - 1]; - l3 = nexthop_info_l3(nh); - iface6_addr_del(iface, &l3->ipv6, l3->prefixlen); - } - addrs = &iface_mcast_addrs[iface->id]; - while (gr_vec_len(addrs->nh) > 0) { - nh = addrs->nh[gr_vec_len(addrs->nh) - 1]; - l3 = nexthop_info_l3(nh); - mcast6_addr_del(iface, &l3->ipv6); - } + ip6_iface_addrs_flush(iface); break; case GR_EVENT_IFACE_STATUS_UP: addrs = &iface_addrs[iface->id]; @@ -512,9 +537,10 @@ static struct gr_module addr6_module = { static struct gr_event_subscription iface_event_subscription = { .callback = ip6_iface_event_handler, - .ev_count = 3, + .ev_count = 4, .ev_types = { GR_EVENT_IFACE_POST_ADD, + GR_EVENT_IFACE_POST_RECONFIG, GR_EVENT_IFACE_PRE_REMOVE, GR_EVENT_IFACE_STATUS_UP, }, diff --git a/smoke/ip6_add_del_test.sh b/smoke/ip6_add_del_test.sh index 2a1891efe..2c97f6ec1 100755 --- a/smoke/ip6_add_del_test.sh +++ b/smoke/ip6_add_del_test.sh @@ -14,3 +14,14 @@ grcli address del 2001::1/64 iface p0 grcli address show grcli address add 2001::1/64 iface p0 grcli address show + +grcli interface add vrf foo +grcli interface set port p0 vrf foo +grcli address show +grcli nexthop show internal + +grcli interface add port p1 devargs net_tap1,iface=x-p1 +grcli interface set port p0 domain p1 +grcli interface set port p1 domain p0 + +grcli nexthop show internal From 7144d91612139d22000e7b0f91c1c604bc9e536c Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Thu, 12 Feb 2026 16:26:58 +0100 Subject: [PATCH 5/6] nexthop: avoid double cleanup on interface suppression When an interface is removed, GR_EVENT_IFACE_PRE_REMOVE is handled by both nexthop_iface_cleanup() in nexthop.c and address-family-specific handlers in ip/control/address.c and ip6/control/address.c (added in 6a1362cc27d7 "ip,ip6: flush addresses on interface mode change"). The event handler execution order is not guaranteed. If nexthop_iface_cleanup() runs first, it destroys local address nexthops by decrementing their ref_count to zero. When the address-family handler runs next, it accesses already-freed nexthops via nexthop_info_l3(), leading to use-after-free. Skip local address nexthops (NH_LOCAL_ADDR_FLAGS) in nh_cleanup_interface_cb(), leaving their cleanup to addr4_delete() and addr6_delete() which properly remove them from the per-interface address vector and handle associated routes. Signed-off-by: Robin Jarry --- modules/infra/control/nexthop.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/infra/control/nexthop.c b/modules/infra/control/nexthop.c index d0e29ba90..32c274fe0 100644 --- a/modules/infra/control/nexthop.c +++ b/modules/infra/control/nexthop.c @@ -442,6 +442,11 @@ struct nexthop *nexthop_lookup_id(uint32_t nh_id) { static void nh_cleanup_interface_cb(struct nexthop *nh, void *priv) { if (nh->iface_id == (uintptr_t)priv) { + if (nh->type == GR_NH_T_L3) { + struct nexthop_info_l3 *l3 = nexthop_info_l3(nh); + if ((l3->flags & NH_LOCAL_ADDR_FLAGS) == NH_LOCAL_ADDR_FLAGS) + return; // addresses are cleaned per address family + } nexthop_routes_cleanup(nh); while (nh->ref_count) nexthop_decref(nh); From e3a165d9226854be75b88a46ef200c08227d8371 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Thu, 12 Feb 2026 17:07:15 +0100 Subject: [PATCH 6/6] bond,port: reassign members/peers to default VRF on destroy When a bond is destroyed, its member ports are detached but remain without a VRF assignment. When a port is destroyed, its peer interfaces (other ports whose domain_id points to this port) lose their domain reference. In both cases, reassign the orphaned ports to the default VRF and fire GR_EVENT_IFACE_POST_RECONFIG so that address-family handlers can flush stale addresses and reinitialize as needed. Export vrf_default_get_or_create() so it can be used from bond and port teardown paths. Signed-off-by: Robin Jarry --- modules/infra/control/bond.c | 11 +++++++++-- modules/infra/control/gr_iface.h | 2 ++ modules/infra/control/iface.c | 4 ++-- modules/infra/control/port.c | 12 ++++++++++++ modules/infra/control/port_test.c | 7 +++++++ modules/infra/control/worker_test.c | 7 +++++++ 6 files changed, 39 insertions(+), 4 deletions(-) diff --git a/modules/infra/control/bond.c b/modules/infra/control/bond.c index 748a9dd83..82249e698 100644 --- a/modules/infra/control/bond.c +++ b/modules/infra/control/bond.c @@ -11,6 +11,7 @@ #include #include #include +#include #include @@ -415,8 +416,14 @@ static int bond_init(struct iface *iface, const void *api_info) { static int bond_fini(struct iface *iface) { struct iface_info_bond *bond = iface_info_bond(iface); - while (bond->n_members > 0) - bond_detach_member(iface, bond->members[bond->n_members - 1].iface); + while (bond->n_members > 0) { + struct iface *member = bond->members[bond->n_members - 1].iface; + bond_detach_member(iface, member); + member->vrf_id = vrf_default_get_or_create(); + if (member->vrf_id != GR_VRF_ID_UNDEF) + vrf_incref(member->vrf_id); + gr_event_push(GR_EVENT_IFACE_POST_RECONFIG, member); + } gr_vec_free(bond->extra_macs); diff --git a/modules/infra/control/gr_iface.h b/modules/infra/control/gr_iface.h index 11af229e0..52f51d0a2 100644 --- a/modules/infra/control/gr_iface.h +++ b/modules/infra/control/gr_iface.h @@ -84,6 +84,8 @@ int iface_set_promisc(struct iface *, bool enabled); uint16_t ifaces_count(gr_iface_type_t type_id); struct iface *iface_next(gr_iface_type_t type_id, const struct iface *prev); +uint16_t vrf_default_get_or_create(void); + // Get the VRF interface. // vrf_id is the VRF interface ID. struct iface *get_vrf_iface(uint16_t vrf_id); diff --git a/modules/infra/control/iface.c b/modules/infra/control/iface.c index 788a999d6..8fae66614 100644 --- a/modules/infra/control/iface.c +++ b/modules/infra/control/iface.c @@ -128,7 +128,7 @@ static int next_ifid(uint16_t *ifid) { } // Get or create the default VRF. Returns GR_VRF_DEFAULT_ID, or 0 on error. -static uint16_t get_or_create_default_vrf(void) { +uint16_t vrf_default_get_or_create(void) { if (iface_from_id(GR_VRF_DEFAULT_ID) != NULL) return GR_VRF_DEFAULT_ID; @@ -174,7 +174,7 @@ struct iface *iface_create(const struct gr_iface *conf, const void *api_info) { // Auto-create default VRF if no VRF specified if (vrf_id == GR_IFACE_ID_UNDEF) { - vrf_id = get_or_create_default_vrf(); + vrf_id = vrf_default_get_or_create(); if (vrf_id == GR_IFACE_ID_UNDEF) goto fail; } diff --git a/modules/infra/control/port.c b/modules/infra/control/port.c index 7c92cbdfa..d75c722e4 100644 --- a/modules/infra/control/port.c +++ b/modules/infra/control/port.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -386,6 +387,17 @@ static int iface_port_fini(struct iface *iface) { return errno_log(-ret, "worker_queue_reassign"); } + while ((i = iface_next(GR_IFACE_TYPE_UNDEF, i)) != NULL) { + if (i->domain_id == iface->id) { + i->vrf_id = vrf_default_get_or_create(); + if (i->vrf_id != GR_VRF_ID_UNDEF) + vrf_incref(i->vrf_id); + i->mode = GR_IFACE_MODE_VRF; + i->domain_id = GR_IFACE_ID_UNDEF; + gr_event_push(GR_EVENT_IFACE_POST_RECONFIG, i); + } + } + port_ifaces[port->port_id] = NULL; free(port->devargs); diff --git a/modules/infra/control/port_test.c b/modules/infra/control/port_test.c index cb35a8907..2b03c72f5 100644 --- a/modules/infra/control/port_test.c +++ b/modules/infra/control/port_test.c @@ -12,6 +12,7 @@ #include #include #include +#include #include // mocked types/functions @@ -29,6 +30,12 @@ struct rte_rcu_qsbr *gr_datapath_rcu(void) { static struct rte_rcu_qsbr rcu; return &rcu; } +uint16_t vrf_default_get_or_create(void) { + return 0; +} +int vrf_incref(uint16_t) { + return 0; +} mock_func(struct iface *, iface_from_id(uint16_t)); mock_func(struct iface *, iface_next(gr_iface_type_t, const struct iface *)); mock_func(int, port_unplug(struct iface_info_port *)); diff --git a/modules/infra/control/worker_test.c b/modules/infra/control/worker_test.c index d4ccd90a1..92e0c59b0 100644 --- a/modules/infra/control/worker_test.c +++ b/modules/infra/control/worker_test.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -50,6 +51,12 @@ struct rte_rcu_qsbr *gr_datapath_rcu(void) { static struct rte_rcu_qsbr rcu; return &rcu; } +uint16_t vrf_default_get_or_create(void) { + return 0; +} +int vrf_incref(uint16_t) { + return 0; +} struct iface *iface_from_id(uint16_t ifid) { return ifid < ARRAY_DIM(ifaces) ? ifaces[ifid] : NULL;