From 5edd2a4489c8b32995eacf8934066eb2f341503b Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Fri, 14 Aug 2026 15:10:57 -0400 Subject: [PATCH] ice: a failed agent must not pin its wake-up deadline in the past contact() updates last_checking_time only after running checks, and its Failed early-return runs none -- so once an agent fails, the deadline poll_timeout() derives from it (last_checking_time + interval) freezes at the last real check, permanently in the past. handle_timeout() then lands back in the same early-return without advancing anything: a driver that re-polls after handling sees the same expired deadline forever. Measured live at ~4.5M loop passes per second, hard enough to starve the driver's whole runtime -- the teardown cascade that would end the failed connection never got to run (found from wosh via polymorph-iroh's freeze drill: SIGSTOP a peer 45s, ICE fails on consent expiry, host wedges solid). Advance the checking clock in the Failed early-return, exactly as the force_candidate_contact reset above already does for the one-shot form of the same disease (issue #88). A failed agent then ticks at the ordinary check cadence until closed or restarted. The regression assertion extends the issue-88 test: after failure, handle_timeout at a time well past the creation-time deadline must not leave an expired deadline armed -- 'well past' because that is where a frozen clock is distinguishable from a fresh one. --- rtc-ice/src/agent/agent_test.rs | 19 +++++++++++++++++++ rtc-ice/src/agent/mod.rs | 9 +++++++++ 2 files changed, 28 insertions(+) diff --git a/rtc-ice/src/agent/agent_test.rs b/rtc-ice/src/agent/agent_test.rs index 5768bc50..9aa01615 100644 --- a/rtc-ice/src/agent/agent_test.rs +++ b/rtc-ice/src/agent/agent_test.rs @@ -2859,6 +2859,25 @@ fn test_handle_inbound_request_defers_failing_connectivity_check() -> Result<()> assert!(a.selected_pair.is_none()); assert!(a.nominated_pair.is_none()); + // A failed agent must not pin `poll_timeout` in the past. `contact` + // runs no checks in `Failed`, but the next wake-up is scheduled as + // `last_checking_time + interval`; if the `Failed` path does not + // advance the clock, every `handle_timeout` leaves the same expired + // deadline armed, and a driver that re-polls after handling spins at + // loop speed (observed live at ~4.5M loop passes per second, + // starving its whole runtime). The periodic twin of the + // `force_candidate_contact` reset asserted above. Asserted well past + // the creation-time deadline, because that is where the frozen clock + // is distinguishable from a fresh one. + let later = Instant::now() + Duration::from_secs(3600); + a.handle_timeout(later)?; + if let Some(deadline) = a.poll_timeout() { + assert!( + deadline > later, + "a failed agent left an expired wake-up deadline armed" + ); + } + a.close()?; Ok(()) } diff --git a/rtc-ice/src/agent/mod.rs b/rtc-ice/src/agent/mod.rs index a1d17086..404cd5a7 100644 --- a/rtc-ice/src/agent/mod.rs +++ b/rtc-ice/src/agent/mod.rs @@ -687,6 +687,15 @@ impl Agent { // The connection is currently failed so don't send any checks // In the future it may be restarted though self.last_connection_state = self.connection_state; + // Advance the checking clock even though no checks run: + // `poll_timeout` schedules the next wake-up as + // `last_checking_time + interval`, so a value frozen at the + // last real check pins the deadline in the past. Every + // `handle_timeout` then lands back here without advancing + // it, and a driver that re-polls after handling spins at + // loop speed, forever. This is the periodic twin of the + // `force_candidate_contact` reset above (issue #88). + self.last_checking_time = now; return; } if self.connection_state == ConnectionState::Checking {