Skip to content

Commit 68e3004

Browse files
committed
fix(sse): saturate exponential backoff to prevent u32 overflow
ExponentialBackoff::retry computed 2u32.pow(current_times) which overflows when current_times >= 32 (panics in debug, wraps in release). Fix: saturate the multiplier at u32::MAX for current_times >= 32. Fixes #1198
1 parent 744b9f9 commit 68e3004

1 file changed

Lines changed: 39 additions & 1 deletion

File tree

crates/rmcp/src/transport/common/client_side_sse.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,15 @@ impl SseRetryPolicy for ExponentialBackoff {
227227
{
228228
return None;
229229
}
230-
Some(self.base_duration * (2u32.pow(current_times as u32)))
230+
// Saturate the multiplier to avoid u32 overflow when current_times >= 32.
231+
// Without saturation, 2u32.pow(32) panics in debug and wraps in release,
232+
// causing either a panic or an unexpectedly short backoff delay.
233+
let multiplier = if current_times >= 32 {
234+
u32::MAX
235+
} else {
236+
2u32.pow(current_times as u32)
237+
};
238+
Some(self.base_duration * multiplier)
231239
}
232240
}
233241

@@ -775,4 +783,34 @@ mod tests {
775783
assert!(stream.next().await.is_none());
776784
assert_eq!(attempts.load(Ordering::Relaxed), 0);
777785
}
786+
787+
#[test]
788+
fn exponential_backoff_saturates_at_high_retry_count() {
789+
let backoff = ExponentialBackoff {
790+
max_times: None,
791+
base_duration: Duration::from_millis(1000),
792+
};
793+
794+
// Normal case: 2^5 = 32 seconds
795+
assert_eq!(backoff.retry(5), Some(Duration::from_millis(32000)));
796+
797+
// At current_times=32, 2u32.pow(32) would overflow.
798+
// Should saturate to u32::MAX instead of panicking.
799+
let result = backoff.retry(32);
800+
assert!(result.is_some(), "should still return Some at retry 32");
801+
assert_eq!(
802+
result,
803+
Some(Duration::from_millis(1000) * u32::MAX),
804+
"should saturate at u32::MAX multiplier"
805+
);
806+
807+
// Even higher values should still work (saturated)
808+
let result = backoff.retry(100);
809+
assert!(result.is_some(), "should still return Some at retry 100");
810+
assert_eq!(
811+
result,
812+
Some(Duration::from_millis(1000) * u32::MAX),
813+
"should remain saturated at u32::MAX"
814+
);
815+
}
778816
}

0 commit comments

Comments
 (0)