Commit 4adc31d14b for frr

commit 4adc31d14b22d0e081e90086abdcb6f008a83d76
Author: Enke Chen <enchen@paloaltonetworks.com>
Date:   Wed Oct 21 16:03:19 2026 -0700

    bgpd: fix default-originate with set-only route-map

    When default-originate is configured with a route-map that has only
    "set" clauses (no "match" clauses), the default route should be
    originated unconditionally with the set clauses applied.

    Previously, the code would walk the entire BGP table looking for a
    route that matches the route-map.  If the table was empty, no match
    would be found and the default route would not be originated, even
    though a set-only route-map should always permit.

    Fix by counting the match rules in the route-map.  If there are no
    match rules (and no "call" statements to other maps that might have
    conditions), skip the table walk and apply the set clauses directly
    to the route attributes.

    This fix also resolves a scalability issue for set-only route-maps:
    previously, any configured route-map (even one with only "set" clauses)
    would trigger a full BGP RIB walk on every route change.  In deployments
    with large routing tables, this O(N) operation per update was expensive.
    By skipping the table walk for set-only route-maps, these cases now
    run in O(1).  Set-only route-maps are common in production deployments
    where operators want to apply attributes (e.g., MED, community, or
    AS-path prepend) to the default route without any conditional logic.

    Implementation details:
    - Count both match clauses and "call" statements - a called map may
      have conditions that require the table walk.
    - Pass the actual default prefix (with correct address family) to
      route_map_apply() so that IPv4/IPv6 specific set operations work
      correctly.
    - Apply the set-only route-map to attr_copy (after the dup) rather
      than new_attr to avoid memory leak - attr_copy will be properly
      flushed after use.
    - Add gshut community AFTER the set-only route-map so that route-map
      set operations cannot overwrite the graceful-shutdown signal.
    - Check the route-map result and do not advertise if the route-map
      denies (e.g., "deny" action with no match clauses).

    Also handle the case where a route-map is configured by name but is
    undefined or empty - treat this as an implicit deny and withdraw the
    default route.

    For backward compatibility, introduce "bgp default-originate
    set-only-unconditional" knob.  By default, the old behavior is
    preserved (set-only route-maps still require at least one table route).
    The new behavior can be enabled with this command.  The default will
    flip in a future release (CONFDATE 20271101), at which point the knob
    will be deprecated.

    Signed-off-by: Enke Chen <enchen@paloaltonetworks.com>

diff --git a/bgpd/bgp_updgrp_adv.c b/bgpd/bgp_updgrp_adv.c
index 295eefd241..76b88e9465 100644
--- a/bgpd/bgp_updgrp_adv.c
+++ b/bgpd/bgp_updgrp_adv.c
@@ -936,6 +936,43 @@ void subgroup_announce_route(struct update_subgroup *subgrp)
 		UNSET_FLAG(subgrp->sflags, SUBGRP_STATUS_FORCE_UPDATES);
 }

+/* Count the matching rules (and calls) in a route-map */
+static int route_map_match_rules(struct route_map *rmap)
+{
+	int count = 0;
+	struct route_map_rule *rule;
+	struct route_map_index *index;
+
+	for (index = rmap->head; index; index = index->next) {
+		/* Count match clauses */
+		for (rule = index->match_list.head; rule; rule = rule->next)
+			count++;
+		/* A "call" to another map may have conditions */
+		if (index->nextrm)
+			count++;
+	}
+
+	return count;
+}
+
+/* Perform the "set" operations in a route-map without "match" config */
+static route_map_result_t bgp_route_map_setting_only(struct bgp *bgp, struct route_map *rmap,
+						     uint8_t rmap_type, const struct prefix *p,
+						     struct attr *attr)
+{
+	struct bgp_path_info rmap_path = { 0 };
+	route_map_result_t ret;
+
+	rmap_path.attr = attr;
+	rmap_path.peer = bgp->peer_self;
+
+	SET_FLAG(bgp->peer_self->rmap_type, rmap_type);
+	ret = route_map_apply(rmap, p, &rmap_path);
+	bgp->peer_self->rmap_type = 0;
+
+	return ret;
+}
+
 void subgroup_default_originate(struct update_subgroup *subgrp, bool withdraw)
 {
 	struct bgp *bgp;
@@ -955,6 +992,8 @@ void subgroup_default_originate(struct update_subgroup *subgrp, bool withdraw)
 	safi_t safi_rib;
 	int pref = 65536;
 	int new_pref = 0;
+	int match_rules;
+	struct route_map *rmap;

 	if (!subgrp)
 		return;
@@ -1000,7 +1039,30 @@ void subgroup_default_originate(struct update_subgroup *subgrp, bool withdraw)
 		attr.mp_nexthop_len = IPV4_MAX_BYTELEN;
 	}

+	rmap = peer->default_rmap[afi][safi].map;
+
 	if (peer->default_rmap[afi][safi].name) {
+		if (!rmap || !rmap->head) {
+			/*
+			 * Route-map is configured but undefined or empty.
+			 * Treat as implicit deny - withdraw the default.
+			 */
+			if (!withdraw &&
+			    !CHECK_FLAG(subgrp->sflags, SUBGRP_STATUS_DEFAULT_ORIGINATE))
+				SET_FLAG(subgrp->sflags, SUBGRP_STATUS_DEFAULT_ORIGINATE);
+			withdraw = true;
+		}
+	}
+
+	match_rules = rmap ? route_map_match_rules(rmap) : 0;
+
+	/*
+	 * Walk the table if the route-map has match clauses, OR if it's a
+	 * set-only route-map but the bgp instance requires at least one
+	 * table route (backward-compatible behavior).
+	 */
+	if (rmap && (match_rules != 0 ||
+		     !CHECK_FLAG(bgp->flags, BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL))) {
 		struct bgp_path_info tmp_pi = {0};

 		tmp_pi.peer = bgp->peer_self;
@@ -1138,11 +1200,6 @@ void subgroup_default_originate(struct update_subgroup *subgrp, bool withdraw)
 				bgp_dest_unlock_node(dest);
 			}

-			/* Advertise the default route */
-			if (bgp_in_graceful_shutdown(bgp) ||
-			    (CHECK_FLAG(peer->flags, PEER_FLAG_GRACEFUL_SHUTDOWN)))
-				bgp_attr_add_gshut_community(new_attr);
-
 			/*
 			 * Prepend local-AS for EBGP. Make a copy first since
 			 * new_attr may be interned (shared) when route-map
@@ -1150,6 +1207,42 @@ void subgroup_default_originate(struct update_subgroup *subgrp, bool withdraw)
 			 * the attribute hash.
 			 */
 			bgp_attr_dup_into(&attr_copy, new_attr);
+
+			/* Apply set-only route-map to attr_copy (not new_attr)
+			 * to avoid memory leak - attr_copy will be flushed.
+			 * Only needed when table walk was skipped (new behavior).
+			 */
+			if (rmap && match_rules == 0 &&
+			    CHECK_FLAG(bgp->flags,
+				       BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL)) {
+				if (bgp_route_map_setting_only(bgp, rmap, PEER_RMAP_TYPE_DEFAULT,
+							       &p, &attr_copy) == RMAP_DENYMATCH) {
+					/*
+					 * Route-map denied - withdraw default.
+					 * The flag may have been cleared by callers,
+					 * so withdraw unconditionally. This may send
+					 * an unnecessary withdraw, but a deny-only
+					 * route-map is not a practical scenario.
+					 */
+					bgp_attr_flush(&attr_copy);
+					aspath_unintern(&aspath);
+					subgroup_default_withdraw_packet(subgrp);
+					UNSET_FLAG(subgrp->sflags, SUBGRP_STATUS_DEFAULT_ORIGINATE);
+					return;
+				}
+				/* Clear reflection attributes - default-originate
+				 * is self-originated, not reflected.
+				 */
+				bgp_attr_clear_reflection(&attr_copy);
+			}
+
+			/* Add gshut community after set-only so route-map
+			 * cannot overwrite the shutdown signal.
+			 */
+			if (bgp_in_graceful_shutdown(bgp) ||
+			    (CHECK_FLAG(peer->flags, PEER_FLAG_GRACEFUL_SHUTDOWN)))
+				bgp_attr_add_gshut_community(&attr_copy);
+
 			bgp_peer_prepend_local_as(peer, afi, safi, &attr_copy);

 			SET_FLAG(subgrp->sflags,
diff --git a/bgpd/bgp_vty.c b/bgpd/bgp_vty.c
index e4b90636ae..00c3160b5c 100644
--- a/bgpd/bgp_vty.c
+++ b/bgpd/bgp_vty.c
@@ -143,6 +143,13 @@ FRR_CFG_DEFAULT_BOOL(BGP_COMPARE_AIGP,
 	{ .val_bool = false },
 );

+#if CONFDATE > 20271101
+CPP_NOTICE("Flip BGP_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL default to true")
+#endif
+FRR_CFG_DEFAULT_BOOL(BGP_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL,
+	{ .val_bool = false },
+);
+
 FRR_CFG_DEFAULT_BOOL(BGP_IPV6_NEXTHOP_PREFER_GLOBAL,
 	{ .val_bool = false },
 );
@@ -774,6 +781,8 @@ int bgp_get_vty(struct bgp **bgp, as_t *as, const char *name,
 			SET_FLAG((*bgp)->flags, BGP_FLAG_RR_ALLOW_OUTBOUND_POLICY);
 		if (DFLT_BGP_COMPARE_AIGP)
 			SET_FLAG((*bgp)->flags, BGP_FLAG_COMPARE_AIGP);
+		if (DFLT_BGP_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL)
+			SET_FLAG((*bgp)->flags, BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL);

 		ret = BGP_SUCCESS;
 	}
@@ -3426,6 +3435,28 @@ DEFUN (no_bgp_deterministic_med,
 	return CMD_SUCCESS;
 }

+#if CONFDATE > 20271101
+CPP_NOTICE("Flip default and deprecate `bgp default-originate set-only-unconditional` knob")
+#endif
+/* "bgp default-originate set-only-unconditional" configuration. */
+DEFPY (bgp_default_originate_setonly_empty_table,
+       bgp_default_originate_setonly_empty_table_cmd,
+       "[no$no] bgp default-originate set-only-unconditional",
+       NO_STR
+       BGP_STR
+       "Control default-originate behavior\n"
+       "Permit set-only route-maps to originate default even with empty table\n")
+{
+	VTY_DECLVAR_CONTEXT(bgp, bgp);
+
+	if (no)
+		UNSET_FLAG(bgp->flags, BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL);
+	else
+		SET_FLAG(bgp->flags, BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL);
+
+	return CMD_SUCCESS;
+}
+
 static int bgp_inst_gr_config_vty(struct vty *vty, struct bgp *bgp, bool on,
 				  bool disable)
 {
@@ -22647,6 +22678,15 @@ int bgp_config_write(struct vty *vty)
 					? ""
 					: "no ");

+		/* BGP default-originate set-only-unconditional. */
+		if (!!CHECK_FLAG(bgp->flags, BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL) !=
+		    SAVE_BGP_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL)
+			vty_out(vty, " %sbgp default-originate set-only-unconditional\n",
+				CHECK_FLAG(bgp->flags,
+					   BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL)
+					? ""
+					: "no ");
+
 		/* BGP update-delay. */
 		bgp_config_write_update_delay(vty, bgp);

@@ -23790,6 +23830,9 @@ void bgp_vty_init(void)
 	install_element(BGP_NODE, &bgp_deterministic_med_cmd);
 	install_element(BGP_NODE, &no_bgp_deterministic_med_cmd);

+	/* "bgp default-originate set-only-unconditional" command */
+	install_element(BGP_NODE, &bgp_default_originate_setonly_empty_table_cmd);
+
 	/* "bgp graceful-restart" command */
 	install_element(BGP_NODE, &bgp_graceful_restart_cmd);
 	install_element(BGP_NODE, &no_bgp_graceful_restart_cmd);
diff --git a/bgpd/bgpd.h b/bgpd/bgpd.h
index 207f341312..ae4da0b923 100644
--- a/bgpd/bgpd.h
+++ b/bgpd/bgpd.h
@@ -793,6 +793,8 @@ struct bgp {
 #define BGP_FLAG_BESTPATH_USE_IMPORTED_ATTRS (1ULL << 47)
 /* Shutdown without sending notification (for HA standby transition) */
 #define BGP_FLAG_SHUTDOWN_NO_NOTIFY (1ULL << 48)
+/* default-originate with set-only route-map works even with empty table */
+#define BGP_FLAG_DEFAULT_ORIGINATE_SETONLY_UNCONDITIONAL (1ULL << 49)

 	/* BGP default address-families.
 	 * New peers inherit enabled afi/safis from bgp instance.