Commit e294dccd01 for frr

commit e294dccd017cf65b55fbbdd3607b871e4263e92a
Author: Donatas Abraitis <donatas@opensourcerouting.org>
Date:   Tue Oct 27 14:50:35 2026 +0200

    bgpd: Move t_llgr_stale event to struct peer_af

    Since t_announce_route is already handled by peer_af as well, let's be consistent
    and keep the same pattern here too.

    The same is happening to t_refresh_stalepath (GR).

    Signed-off-by: Donatas Abraitis <donatas@opensourcerouting.org>

diff --git a/bgpd/bgp_fsm.c b/bgpd/bgp_fsm.c
index faf3578124..128b9133c5 100644
--- a/bgpd/bgp_fsm.c
+++ b/bgpd/bgp_fsm.c
@@ -366,6 +366,7 @@ void bgp_timer_set(struct peer_connection *connection)
 {
 	afi_t afi;
 	safi_t safi;
+	struct peer_af *paf;
 	struct peer *peer = connection->peer;

 	switch (connection->status) {
@@ -494,8 +495,11 @@ void bgp_timer_set(struct peer_connection *connection)
 		event_cancel(&connection->t_gr_restart);
 		event_cancel(&connection->t_gr_stale);

-		FOREACH_AFI_SAFI (afi, safi)
-			event_cancel(&peer->t_llgr_stale[afi][safi]);
+		FOREACH_AFI_SAFI (afi, safi) {
+			paf = peer_af_find(peer, afi, safi);
+			if (paf)
+				event_cancel(&paf->t_llgr_stale);
+		}

 		event_cancel(&connection->t_pmax_restart);
 		event_cancel(&peer->t_refresh_stalepath);
@@ -858,9 +862,8 @@ static void bgp_graceful_restart_timer_expire(struct event *event)
 			bgp_set_llgr_stale(peer, afi, safi);
 			bgp_clear_stale_route(peer, afi, safi);

-			event_add_timer(bm->master, bgp_llgr_stale_timer_expire,
-					paf, peer->llgr[afi][safi].stale_time,
-					&peer->t_llgr_stale[afi][safi]);
+			event_add_timer(bm->master, bgp_llgr_stale_timer_expire, paf,
+					peer->llgr[afi][safi].stale_time, &paf->t_llgr_stale);

 			for (ALL_LIST_ELEMENTS(peer->bgp->peer, node, nnode,
 					       tmp_peer))
@@ -877,6 +880,7 @@ static void bgp_graceful_stale_timer_expire(struct event *event)
 {
 	struct peer_connection *connection = EVENT_ARG(event);
 	struct peer *peer = connection->peer;
+	struct peer_af *paf;
 	afi_t afi;
 	safi_t safi;

@@ -888,7 +892,7 @@ static void bgp_graceful_stale_timer_expire(struct event *event)
 	 * RFC 9494 §4.3: for LLGR-negotiated AFI/SAFI, retention is bounded
 	 * by the Long-Lived Stale Time, not stalepath-time. Skip the delete
 	 * if LLGR retention is (or will be) active for this AFI/SAFI:
-	 *   - t_llgr_stale[afi][safi] scheduled: the LLGR window is already
+	 *   - paf->t_llgr_stale scheduled: the LLGR window is already
 	 *     running; t_gr_restart has fired but didn't cancel t_gr_stale
 	 *     (bgp_graceful_restart_timer_off() bails when any AFI/SAFI is
 	 *     in PEER_STATUS_LLGR_WAIT).
@@ -904,7 +908,8 @@ static void bgp_graceful_stale_timer_expire(struct event *event)
 		if (!peer->nsf[afi][safi])
 			continue;

-		if (event_is_scheduled(peer->t_llgr_stale[afi][safi]) ||
+		paf = peer_af_find(peer, afi, safi);
+		if ((paf && event_is_scheduled(paf->t_llgr_stale)) ||
 		    (peer->llgr[afi][safi].stale_time &&
 		     event_is_scheduled(connection->t_gr_restart))) {
 			if (bgp_debug_neighbor_events(peer))
@@ -3010,6 +3015,7 @@ bgp_establish(struct peer_connection *connection)
 {
 	afi_t afi;
 	safi_t safi;
+	struct peer_af *paf;
 	enum bgp_fsm_state_progress ret = BGP_FSM_SUCCESS;
 	struct peer *other;
 	struct peer *peer = connection->peer;
@@ -3093,8 +3099,9 @@ bgp_establish(struct peer_connection *connection)
 	 * Stop Long-lived Graceful Restart timers.
 	 */
 	FOREACH_AFI_SAFI (afi, safi) {
-		if (event_is_scheduled(peer->t_llgr_stale[afi][safi])) {
-			event_cancel(&peer->t_llgr_stale[afi][safi]);
+		paf = peer_af_find(peer, afi, safi);
+		if (paf && event_is_scheduled(paf->t_llgr_stale)) {
+			event_cancel(&paf->t_llgr_stale);
 			if (bgp_debug_neighbor_events(peer))
 				zlog_debug("%pBP Long-lived stale timer stopped for afi/safi: %d/%d for %s",
 					   peer, afi, safi,
diff --git a/bgpd/bgp_route.c b/bgpd/bgp_route.c
index 42710fc94d..3bd3b5da3f 100644
--- a/bgpd/bgp_route.c
+++ b/bgpd/bgp_route.c
@@ -13781,6 +13781,7 @@ void route_vty_out_detail(struct vty *vty, struct bgp *bgp, struct bgp_dest *bn,
 	int first = 0;
 	struct listnode *node, *nnode;
 	struct peer *peer;
+	struct peer_af *paf;
 	bool addpath_capable;
 	int has_adj;
 	unsigned int first_as;
@@ -14919,12 +14920,10 @@ skip_nexthop:
 				gr_remaining);
 	}

-	if (event_is_scheduled(path->peer->t_llgr_stale[afi][safi]) &&
-	    bgp_attr_get_community(attr) &&
-	    community_include(bgp_attr_get_community(attr),
-			      COMMUNITY_LLGR_STALE)) {
-		unsigned long llgr_remaining = event_timer_remain_second(
-			path->peer->t_llgr_stale[afi][safi]);
+	paf = peer_af_find(path->peer, afi, safi);
+	if (paf && event_is_scheduled(paf->t_llgr_stale) && bgp_attr_get_community(attr) &&
+	    community_include(bgp_attr_get_community(attr), COMMUNITY_LLGR_STALE)) {
+		unsigned long llgr_remaining = event_timer_remain_second(paf->t_llgr_stale);

 		if (json_paths) {
 			json_object_int_add(json_path, "llgrSecondsRemaining",
diff --git a/bgpd/bgpd.c b/bgpd/bgpd.c
index e5d6403a15..fe22cef974 100644
--- a/bgpd/bgpd.c
+++ b/bgpd/bgpd.c
@@ -1088,7 +1088,7 @@ int peer_af_delete(struct peer *peer, afi_t afi, safi_t safi)
 	bgp_soft_reconfig_table_task_cancel(bgp, bgp->rib[afi][safi], peer);

 	bgp_stop_announce_route_timer(af);
-	event_cancel(&peer->t_llgr_stale[afi][safi]);
+	event_cancel(&af->t_llgr_stale);

 	if (PAF_SUBGRP(af)) {
 		if (BGP_DEBUG(update_groups, UPDATE_GROUPS))
@@ -3121,13 +3121,16 @@ void peer_nsf_stop(struct peer *peer)
 {
 	afi_t afi;
 	safi_t safi;
+	struct peer_af *paf;

 	UNSET_FLAG(peer->sflags, PEER_STATUS_NSF_WAIT);
 	UNSET_FLAG(peer->sflags, PEER_STATUS_NSF_MODE);

 	FOREACH_AFI_SAFI_NSF (afi, safi) {
 		peer->nsf[afi][safi] = 0;
-		event_cancel(&peer->t_llgr_stale[afi][safi]);
+		paf = peer_af_find(peer, afi, safi);
+		if (paf)
+			event_cancel(&paf->t_llgr_stale);
 	}

 	if (event_is_scheduled(peer->connection->t_gr_restart)) {
diff --git a/bgpd/bgpd.h b/bgpd/bgpd.h
index 77ede3806e..207f341312 100644
--- a/bgpd/bgpd.h
+++ b/bgpd/bgpd.h
@@ -1392,6 +1392,9 @@ struct peer_af {
 	 */
 	struct event *t_announce_route;

+	/* Long-lived Graceful Restart "Long-lived Stale Time" timer */
+	struct event *t_llgr_stale;
+
 	afi_t afi;
 	safi_t safi;
 	int afid;
@@ -2058,7 +2061,6 @@ struct peer {
 	_Atomic uint32_t v_gr_restart;

 	/* Threads. */
-	struct event *t_llgr_stale[AFI_MAX][SAFI_MAX];
 	struct event *t_refresh_stalepath;

 	/* Thread flags. */
diff --git a/tests/topotests/bgp_llgr_stale_timer_af_delete/test_bgp_llgr_stale_timer_af_delete.py b/tests/topotests/bgp_llgr_stale_timer_af_delete/test_bgp_llgr_stale_timer_af_delete.py
index 4d3846dd35..088a8314ad 100644
--- a/tests/topotests/bgp_llgr_stale_timer_af_delete/test_bgp_llgr_stale_timer_af_delete.py
+++ b/tests/topotests/bgp_llgr_stale_timer_af_delete/test_bgp_llgr_stale_timer_af_delete.py
@@ -4,9 +4,9 @@
 """
 Regression test for stale BGP LLGR timers after peer AF deletion.

-The test arms peer->t_llgr_stale[afi][safi], deletes the peer AF that was used
-as the timer callback argument, and verifies that bgpd survives beyond the
-original timer deadline.
+The test arms paf->t_llgr_stale, deletes the peer AF that owns the timer (and
+is used as the timer callback argument), and verifies that bgpd survives beyond
+the original timer deadline.
 """

 import functools
@@ -94,7 +94,7 @@ def test_bgp_llgr_stale_timer_cancelled_on_peer_af_delete():
     Deleting a peer AF must cancel any LLGR stale timer using that peer_af.

     Broken behavior:
-    - GR helper mode arms peer->t_llgr_stale[afi][safi] with struct peer_af.
+    - GR helper mode arms paf->t_llgr_stale with struct peer_af as argument.
     - `no neighbor ... activate` deletes and frees that peer_af.
     - The stale timer later fires and dereferences the freed callback argument.
     """