Commit 57a7f3eb48 for frr

commit 57a7f3eb48837359d6e3ee5ebd4acea8f49320e5
Author: Srinivasan Koona Lokabiraman <srinivasan@nexthop.ai>
Date:   Fri Oct 23 08:33:44 2026 +0000

    bgpd: recover from SendHoldTimer expiry instead of wedging the peer

    When the SendHoldTimer (RFC 9687) expires,
    bgp_event_stop_with_notify() called bgp_stop() inline. bgp_stop()
    runs event_cancel_event_ready(), which cancels the BGP_Stop event
    that bgp_write_notify() had just queued to advance the FSM, and it
    does not itself change state. The peer is therefore left stuck in
    Established with a closed fd (fd == -1) and no ConnectRetry timer --
    it never resets or reconnects.

    Send the notification and let the FSM dispatch the queued BGP_Stop
    (or TCP_fatal_error) so the peer tears down and reconnects like
    every other stop.

    Signed-off-by: Srinivasan Koona Lokabiraman <srinivasan@nexthop.ai>

diff --git a/bgpd/bgp_fsm.c b/bgpd/bgp_fsm.c
index 73dc7979a2..be7bb654a8 100644
--- a/bgpd/bgp_fsm.c
+++ b/bgpd/bgp_fsm.c
@@ -3932,5 +3932,15 @@ void bgp_event_stop_with_notify(struct event *event)
 {
 	struct peer_connection *connection = EVENT_ARG(event);

-	bgp_stop_with_notify(connection, BGP_NOTIFY_SEND_HOLD_ERR, 0);
+	/*
+	 * Drive the teardown through the FSM rather than stopping the peer
+	 * inline.  bgp_notify_send() -> bgp_write_notify() queues a BGP_Stop
+	 * (or TCP_fatal_error, if the stuck send queue rejects the write); the
+	 * FSM then advances the state and re-arms the ConnectRetry timer like
+	 * every other stop.  Calling bgp_stop() directly here (as
+	 * bgp_stop_with_notify() does) runs event_cancel_event_ready(), which
+	 * cancels that queued event, so the state is never advanced and the peer
+	 * is left stuck in Established with fd == -1 -- a zombie session.
+	 */
+	bgp_notify_send(connection, BGP_NOTIFY_SEND_HOLD_ERR, 0);
 }