Skip to content

bgpd: Fix stale EVPN type-5 route for suppressed prefix during L3VNI bounce - #21992

Merged
riw777 merged 2 commits into
FRRouting:masterfrom
raja-rajasekar:rajasekarr/evpn_removed_type5
Jun 3, 2026
Merged

bgpd: Fix stale EVPN type-5 route for suppressed prefix during L3VNI bounce#21992
riw777 merged 2 commits into
FRRouting:masterfrom
raja-rajasekar:rajasekarr/evpn_removed_type5

Conversation

@raja-rajasekar

@raja-rajasekar raja-rajasekar commented May 19, 2026

Copy link
Copy Markdown
Contributor

bgpd: Fix stale EVPN type-5 route for suppressed prefix during L3VNI bounce

bgp_evpn_advertise_type5_routes() does not check BGP_PATH_REMOVED flag when walking the VRF table.
During interface flaps with concurrent L3VNI bounce, a route can be in a transient state where it is:
  - BGP_PATH_SELECTED (bgp_process() deferred, hasn't cleared it yet)
  - BGP_PATH_REMOVED (redistribute_delete marked it for deletion)
  - UNSUPPRESSED (bgp_aggregate_decrement removed it from aggregate)

When L3VNI add triggers bgp_evpn_advertise_type5_routes(), such a route passes the SELECTED and injectable checks and gets advertised into EVPN as a type-5 route. Once the interface comes back up and the route gets re-suppressed by the aggregate, no subsequent withdraw or advertise walk will touch it (suppressed routes are skipped), leaving a stale type-5 permanently in the EVPN table.

bgpd: Withdraw type-5 routes regardless of aggregate suppression state

bgp_evpn_withdraw_type5_routes() skips suppressed routes because it uses
is_route_injectable_into_evpn() which filters on suppression. If a route
was advertised while unsuppressed then re-suppressed before the withdraw
walk runs, its stale type-5 is never cleaned up.

Fix is to use is_route_injectable_into_evpn_non_supp() instead,
consistent with bgp_aggr_supp_withdraw_from_evpn() which already
uses this variant.

A topotest for this issue is not feasible since it requires a precise race condition within a ~35ms window that is near impossible to reproduce deterministically. It was hit once in internal testing and root-caused through LTTng trace analysis. Please refer #21992 (comment)

…bounce

bgp_evpn_advertise_type5_routes() does not check BGP_PATH_REMOVED flag
when walking the VRF table.
During interface flaps with concurrent L3VNI bounce, a route can be in
a transient state where it is:
  - BGP_PATH_SELECTED (bgp_process() deferred, hasn't cleared it yet)
  - BGP_PATH_REMOVED (redistribute_delete marked it for deletion)
  - UNSUPPRESSED (bgp_aggregate_decrement removed it from aggregate)

When L3VNI add triggers bgp_evpn_advertise_type5_routes(), such a route
passes the SELECTED and injectable checks and gets advertised into EVPN
as a type-5 route. Once the interface comes back up and the route gets
re-suppressed by the aggregate, no subsequent withdraw or advertise walk
will touch it (suppressed routes are skipped), leaving a stale type-5
permanently in the EVPN table.

Signed-off-by: Rajasekar Raja <rajasekarr@nvidia.com>
@greptile-apps

greptile-apps Bot commented May 19, 2026

Copy link
Copy Markdown

Greptile Summary

This one-line guard prevents stale EVPN type-5 routes during L3VNI bounce by skipping paths marked BGP_PATH_REMOVED before they can be re-advertised into the EVPN table.

  • Root cause patched: bgp_evpn_advertise_type5_routes() previously walked the VRF table without checking BGP_PATH_REMOVED, so a transiently SELECTED+REMOVED+UNSUPPRESSED route could slip through during a concurrent L3VNI add, producing a type-5 entry that nothing would ever withdraw once the route was re-suppressed.
  • Fix placement: The new CHECK_FLAG(pi->flags, BGP_PATH_REMOVED) guard is inserted as the first condition in the advertise path, which is the correct place β€” the companion bgp_evpn_withdraw_type5_routes() already skips stale withdraws safely, and the existing injection/suppression checks in is_route_injectable_into_evpn() were not designed to catch REMOVED state.

Confidence Score: 5/5

Safe to merge β€” the change is a minimal, targeted guard in a single function with no side effects on adjacent code paths.

The fix is a two-line conditional inserted at exactly the right point in the advertise walk. It mirrors the BGP_PATH_REMOVED checks used throughout the rest of bgp_evpn.c and bgp_route.c, the companion withdraw function is unaffected, and the is_route_injectable_into_evpn() suppression check that precedes it is unchanged.

No files require special attention.

Important Files Changed

Filename Overview
bgpd/bgp_evpn.c Single-line guard added to bgp_evpn_advertise_type5_routes() to skip paths with BGP_PATH_REMOVED; fix is minimal, correctly placed, and matches the pattern used throughout the rest of the file.

Sequence Diagram

sequenceDiagram
    participant IF as Interface
    participant RD as redistribute_delete
    participant ADV as bgp_evpn_advertise_type5_routes

    IF->>RD: Interface flap sets BGP_PATH_REMOVED
    Note over RD: BGP_PATH_SELECTED still set (bgp_process deferred)
    RD->>ADV: L3VNI bounce triggers advertise walk

    alt BEFORE fix
        ADV->>ADV: SELECTED check passes, type-5 advertised (stale)
    else AFTER fix
        ADV->>ADV: REMOVED check skips route correctly
    end
Loading

Reviews (1): Last reviewed commit: "bgpd: Fix stale EVPN type-5 route for su..." | Re-trigger Greptile

@raja-rajasekar

Copy link
Copy Markdown
Contributor Author

NOTE:Β Β Everything below happens in ~35ms timeframe (So, a race condition or a timing issue)

Problem:
----------------------------------------------------------------------
frr.show_running
router bgp 65000 vrf RED
 address-family ipv4 unicast
  aggregate-address 192.168.1.0/24 summary-only >>>>>>>>>>>>>>>>>>>>
 address-family l2vpn evpn
  advertise ipv4 unicast

bgp.route
suppressed correctly
 s> 192.168.1.12/31   0.0.0.0(l1-p1) >>>>>>>> SUPPRESSED
 s> 192.168.1.4/31    0.0.0.0(l1-p1)
 s> 192.168.1.6/31    0.0.0.0(l1-p1)
 s> 192.168.1.8/31    0.0.0.0(l1-p1)
 s> 192.168.1.10/31   0.0.0.0(l1-p1)

bgp.evpn.route
Route Distinguisher: 10.32.9.32:1000
 *> [5]:[0]:[24]:[192.168.1.0] RD 10.32.9.32:1000
                    10.32.9.32 (l1-p1)
                    ET:8 RT:32826:1000 Rmac:b0:cf:0e:75:0d:ff 
 *> [5]:[0]:[31]:[192.168.1.12] RD 10.32.9.32:1000 >>>>>>>>>>>>>>>> BUG!!!!!! THIS SHOULD NOT BE PRESENT
                    10.32.9.32 (l1-p1)
                    ET:8 RT:32826:1000 Rmac:b0:cf:0e:75:0d:ff


STEP 1: swp4s0 goes DOWN (14:56:40.150)
----------------------------------------------------------------------
## Kernel brings swp4s0 down. Zebra notifies BGP. The connected route 192.168.1.12/31 is about to be removed from BGP.

2026-05-14T14:56:40.150 frr_bgp:bgp_ifp_oper {'vrf_id': 135, 'interface': 'swp4s0', 'location': 'Intf DOWN'}
2026-05-14T14:56:40.150 frr_bgp:interface_address_oper_zrecv {'vrf_id': 135, 'ifname': 'swp4s0', 'address': '192.168.1.12', 'location': 'Rx Intf address Delete'}


STEP 2: redistribute_delete for 192.168.1.12 connected route (14:56:40.151)
----------------------------------------------------------------------
## State of 192.168.1.12/31 after this:
##   - UNSUPPRESSED (aggregate decremented)
##   - BGP_PATH_SELECTED = YES (Enqueued in METAQ but not dequeued to run bestpath and  clear this)
##   - BGP_PATH_REMOVED = YES (marked for deletion)

2026-05-14T14:56:40.151 frr_bgp:bgp_redistribute_delete_zrecv {'vrf_id': 135, 'prefix': '192.168.1.12', 'type': 2, 'instance': 0}
2026-05-14T14:56:40.151 frr_bgp:bgp_redistribute_delete_zrecv {'vrf_id': 135, 'prefix': '192.168.1.12', 'type': 2, 'instance': 0}


STEP 3: L3VNI 1000 DELETE (14:56:40.166)
----------------------------------------------------------------------
## Has no effect on 192.168.1.12/31

2026-05-14T14:56:40.166 frr_bgp:bgp_ifp_oper {'vrf_id': 0, 'interface': 'vlan351_l3', 'location': 'Intf DOWN'}
2026-05-14T14:56:40.166 frr_bgp:evpn_local_l3vni_del_zrecv {'vni': 1000, 'vrf': 135}
2026-05-14T14:56:40.166 frr_bgp:bgp_ifp_oper {'vrf_id': 135, 'interface': 'vlan351_l3', 'location': 'Intf DOWN'}


STEP 4: L3VNI 1000 ADD (14:56:40.187) --> triggers BUG
----------------------------------------------------------------------
## vlan351_l3 comes back up β†’ triggers evpn_local_l3vni_add from Zebra.
## Inside bgp_evpn_local_l3vni_add():
##   Calls update_advertise_vrf_routes() β†’ bgp_evpn_advertise_type5_routes()
##   This walks the ENTIRE IPv4 unicast table for VRF RED and
##   advertises every route that passes these checks:
##     1. CHECK_FLAG(pi->flags, BGP_PATH_SELECTED) β€” YES for 192.168.1.12/31 (not cleared yet!)
##     2. is_route_injectable_into_evpn(pi) β€” YES for .12 (it's UNSUPPRESSED)
##     3. Does NOT check BGP_PATH_REMOVED β€” THIS IS BUG!!!!!!!!
##
## So 192.168.1.12/31 gets advertised into EVPN even though it's logically deleted.

2026-05-14T14:56:40.187 frr_bgp:bgp_ifp_oper {'vrf_id': 135, 'interface': 'vlan351_l3', 'location': 'Intf UP'}
2026-05-14T14:56:40.187 frr_bgp:evpn_local_l3vni_add_zrecv {'vni': 1000, 'vrf': 135, 'svi_rmac': 'b0:cf:0e:75:0d:ff', 'vrr_rmac': 'b0:cf:0e:75:0d:ff', 'vtep': '10.32.9.32', 'filter': 0, 'svi_ifindex': 138, 'anycast_mac': 'no'}

## Aggregate /24 advertised (correct β€” this is the summary route):
2026-05-14T14:56:40.187 frr_bgp:evpn_advertise_type5 {'vrf_id': 135, 'ip': '192.168.1.0', 'rmac': 'b0:cf:0e:75:0d:ff', 'vtep': '10.32.9.32'}

## Routes .0/.2/.4/.6 are still properly SUPPRESSED by the aggregate(Their interfaces stayed up):
2026-05-14T14:56:40.187 frr_bgp:evpn_ignore_suppress_route {'prefix': '192.168.1.0/31', 'peer': 'Static announcement'}
2026-05-14T14:56:40.187 frr_bgp:evpn_ignore_suppress_route {'prefix': '192.168.1.2/31', 'peer': 'Static announcement'}
2026-05-14T14:56:40.187 frr_bgp:evpn_ignore_suppress_route {'prefix': '192.168.1.4/31', 'peer': 'Static announcement'}
2026-05-14T14:56:40.187 frr_bgp:evpn_ignore_suppress_route {'prefix': '192.168.1.6/31', 'peer': 'Static announcement'}

## Routes .8/.10/.12/.14/.16/.18 are UNSUPPRESSED (from Step 2 redistribute_delete)
## and still BGP_PATH_SELECTED (bgp_process hasn't run) β†’ ALL ADVERTISED:
2026-05-14T14:56:40.187 frr_bgp:evpn_advertise_type5 {'vrf_id': 135, 'ip': '192.168.1.8', 'rmac': 'b0:cf:0e:75:0d:ff', 'vtep': '10.32.9.32'}
2026-05-14T14:56:40.187 frr_bgp:evpn_advertise_type5 {'vrf_id': 135, 'ip': '192.168.1.10', 'rmac': 'b0:cf:0e:75:0d:ff', 'vtep': '10.32.9.32'}
2026-05-14T14:56:40.187 frr_bgp:evpn_advertise_type5 {'vrf_id': 135, 'ip': '192.168.1.12', 'rmac': 'b0:cf:0e:75:0d:ff', 'vtep': '10.32.9.32'}   ----> ****** BUG : THIS SHOULD NOT BE ADVERTISED (BGP_PATH_REMOVED) ******

2026-05-14T14:56:40.187 frr_bgp:evpn_advertise_type5 {'vrf_id': 135, 'ip': '192.168.1.14', 'rmac': 'b0:cf:0e:75:0d:ff', 'vtep': '10.32.9.32'}
2026-05-14T14:56:40.187 frr_bgp:evpn_advertise_type5 {'vrf_id': 135, 'ip': '192.168.1.16', 'rmac': 'b0:cf:0e:75:0d:ff', 'vtep': '10.32.9.32'}
2026-05-14T14:56:40.187 frr_bgp:evpn_advertise_type5 {'vrf_id': 135, 'ip': '192.168.1.18', 'rmac': 'b0:cf:0e:75:0d:ff', 'vtep': '10.32.9.32'}

bgp_evpn_withdraw_type5_routes() skips suppressed routes because it uses
is_route_injectable_into_evpn() which filters on suppression. If a route
was advertised while unsuppressed then re-suppressed before the withdraw
walk runs, its stale type-5 is never cleaned up.

Fix is to use is_route_injectable_into_evpn_non_supp() instead,
consistent with bgp_aggr_supp_withdraw_from_evpn() which already
uses this variant.

Signed-off-by: Rajasekar Raja <rajasekarr@nvidia.com>
@github-actions github-actions Bot added size/S and removed size/XS labels May 19, 2026
@riw777

riw777 commented May 20, 2026

Copy link
Copy Markdown
Member

ci failure is unrelated ...

@riw777 riw777 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good

@riw777 riw777 added the bugfix label May 20, 2026
@riw777
riw777 merged commit d6c9a4b into FRRouting:master Jun 3, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants