Commit 890d9f0f73 for frr

commit 890d9f0f7360012cce9f354c582c4420590b4be8
Author: Vaideesh Ravi Shankar <vaideeshr@nvidia.com>
Date:   Thu Oct 15 10:31:29 2026 -0700

    bgpd: withdraw route from zebra when no nexthop is left

    When bgpd sends a route to zebra, it checks the table-map for each
    path of the route. If the table-map denies every path, no nexthop is
    left.

    Before commit d7144594cab0 ("bgpd: Drop unnecessary is_add check for
    bgp_zebra_announce_actual()"), bgpd sent a DELETE to zebra in this
    case. Now it sends an ADD with no nexthops. Zebra rejects this ADD
    and logs a warning for each route:

    zread_route_add: received a route without nexthops for prefix
    (default:0)10.0.0.1/32 from client bgp

    This causes two problems:

    1. Main problem: If the route was installed before, zebra keeps the old
       route. For example, in bgpd a route gets a new community and the
       table-map now denies it. The route should be removed, but it stays
       in the RIB and in the kernel.

    2. Secondary problem: With a large BGP table and a table-map that denies
       most routes, zebra logs this warning for every denied route. The log
       file fills up and rotates very often.

    Fix: when no nexthop is left, call bgp_zebra_withdraw_actual() to
    send a DELETE, instead of sending an ADD with no nexthops

    Add a test to bgp_table_map:
    1. r1 receives 10.0.0.1/32 from r2 and installs it in zebra.
    2. r2 adds community 65002:999 to this route.
    3. The table-map on r1 denies routes with this community, so r1
       must remove the route from zebra.
    4. r2 removes the community, and r1 installs the route again.

    Without this fix, step 3 fails: the route stays in zebra's RIB on r1.

    Signed-off-by: Vaideesh Ravi Shankar <vaideeshr@nvidia.com>

diff --git a/bgpd/bgp_zebra.c b/bgpd/bgp_zebra.c
index 22dc36d622..c2579d13a1 100644
--- a/bgpd/bgp_zebra.c
+++ b/bgpd/bgp_zebra.c
@@ -1653,6 +1653,15 @@ enum zclient_send_status bgp_zebra_announce_actual(struct bgp_dest *dest,
 					 table->safi, &nhg_id, &metric, &tag, &allow_recursion,
 					 do_wt_ecmp);

+	/*
+	 * No usable nexthop is left (e.g. the table-map denied every path).
+	 * Withdraw the route instead of sending an ADD without nexthops:
+	 * zebra rejects such an ADD, logs a warning and keeps the previously
+	 * installed route, which then stays stale in the RIB/FIB.
+	 */
+	if (!valid_nh_count && !nhg_id)
+		return bgp_zebra_withdraw_actual(dest, info, bgp);
+
 	if (do_wt_ecmp == BGP_WECMP_BEHAVIOR_USE_RECURSIVE_VALUE)
 		SET_FLAG(api.flags, ZEBRA_FLAG_USE_RECURSIVE_WEIGHT);

diff --git a/tests/topotests/bgp_table_map/r1/frr.conf b/tests/topotests/bgp_table_map/r1/frr.conf
index f74440c384..5e5fda7c28 100644
--- a/tests/topotests/bgp_table_map/r1/frr.conf
+++ b/tests/topotests/bgp_table_map/r1/frr.conf
@@ -4,6 +4,12 @@ int r1-eth0
 !
 access-list AccList seq 5 permit 10.0.0.1/32
 !
+bgp community-list standard Block seq 5 permit 65002:999
+!
+route-map TableMap deny 5
+ match community Block
+exit
+!
 route-map TableMap permit 10
  match ip address AccList
 exit
diff --git a/tests/topotests/bgp_table_map/r2/frr.conf b/tests/topotests/bgp_table_map/r2/frr.conf
index 4523fe49ea..65af90557d 100644
--- a/tests/topotests/bgp_table_map/r2/frr.conf
+++ b/tests/topotests/bgp_table_map/r2/frr.conf
@@ -2,6 +2,10 @@
 int r2-eth0
  ip address 10.255.0.2/24
 !
+route-map SetBlock permit 10
+ set community 65002:999
+exit
+!
 router bgp 65002
  bgp router-id 10.255.0.2
  no bgp ebgp-requires-policy
diff --git a/tests/topotests/bgp_table_map/test_bgp_table_map.py b/tests/topotests/bgp_table_map/test_bgp_table_map.py
index 30ec4425cd..3602b5a656 100644
--- a/tests/topotests/bgp_table_map/test_bgp_table_map.py
+++ b/tests/topotests/bgp_table_map/test_bgp_table_map.py
@@ -122,6 +122,86 @@ def test_bgp_table_map():
     assert result is None, "Should contain only one of two shared networks"


+def test_bgp_table_map_deny_after_attribute_change():
+    """
+    A route installed in zebra must be withdrawn when an attribute change
+    makes the table-map deny it, instead of staying stale in the RIB/FIB.
+    """
+    tgen = get_topogen()
+
+    if tgen.routers_have_failure():
+        pytest.skip(tgen.errors)
+
+    r1 = tgen.gears["r1"]
+    r2 = tgen.gears["r2"]
+
+    def _bgp_route_in_rib(installed):
+        output = json.loads(r1.vtysh_cmd("show ip route 10.0.0.1/32 json"))
+        if installed:
+            expected = {
+                "10.0.0.1/32": [
+                    {"protocol": "bgp", "selected": True, "installed": True}
+                ]
+            }
+        else:
+            expected = {"10.0.0.1/32": None}
+
+        return topotest.json_cmp(output, expected)
+
+    def _bgp_route_community(community):
+        output = json.loads(r1.vtysh_cmd("show bgp ipv4 unicast 10.0.0.1/32 json"))
+        if community:
+            expected = {"paths": [{"community": {"string": community}}]}
+        else:
+            expected = {"paths": [{"community": None}]}
+
+        return topotest.json_cmp(output, expected)
+
+    test_func = functools.partial(_bgp_route_in_rib, True)
+    _, result = topotest.run_and_expect(test_func, None, count=20, wait=1)
+    assert result is None, "10.0.0.1/32 should be installed before the change"
+
+    #
+    # Tag 10.0.0.1/32 with a community the table-map denies
+    #
+    r2.vtysh_cmd(
+        """
+        configure terminal
+          router bgp 65002
+            address-family ipv4 unicast
+              network 10.0.0.1/32 route-map SetBlock
+        """
+    )
+
+    test_func = functools.partial(_bgp_route_community, "65002:999")
+    _, result = topotest.run_and_expect(test_func, None, count=20, wait=1)
+    assert result is None, "10.0.0.1/32 should be received with community 65002:999"
+
+    test_func = functools.partial(_bgp_route_in_rib, False)
+    _, result = topotest.run_and_expect(test_func, None, count=20, wait=1)
+    assert result is None, "10.0.0.1/32 denied by table-map is still in the RIB (stale)"
+
+    #
+    # Remove the community, 10.0.0.1/32 is permitted again
+    #
+    r2.vtysh_cmd(
+        """
+        configure terminal
+          router bgp 65002
+            address-family ipv4 unicast
+              network 10.0.0.1/32
+        """
+    )
+
+    test_func = functools.partial(_bgp_route_community, None)
+    _, result = topotest.run_and_expect(test_func, None, count=20, wait=1)
+    assert result is None, "10.0.0.1/32 should be received without community"
+
+    test_func = functools.partial(_bgp_route_in_rib, True)
+    _, result = topotest.run_and_expect(test_func, None, count=20, wait=1)
+    assert result is None, "10.0.0.1/32 should be installed again"
+
+
 if __name__ == "__main__":
     args = ["-s"] + sys.argv[1:]
     sys.exit(pytest.main(args))