Describe the bug
Multi::wait() and Multi::poll() panic with "attempt to add with overflow" when called with a valid Duration with secs == 2_147_483 and nanos >= 648_000_000.
The panic is caused by an off-by-one in the overflow guard inside timeout_i32().
Root cause
timeout_i32 (src/multi.rs:625-633) is meant to convert a Duration to an i32 millisecond count, clamping at i32::MAX:
fn timeout_i32(timeout: Duration) -> i32 {
let secs = timeout.as_secs();
if secs > (i32::MAX / 1000) as u64 { // bug: should use `>=` instead of `>`
i32::MAX
} else {
secs as i32 * 1000 + timeout.subsec_nanos() as i32 / 1_000_000
}
}
The guard uses > instead of >=. i32::MAX / 1000 == 2_147_483, so secs == 2_147_483 passes through to the else branch. 2_147_483 * 1000 == 2_147_483_000 fits in i32, but when nanos >= 648_000_000, 2_147_483_000 + 648 = 2_147_483_648 > i32::MAX (2_147_483_647), which overflows and causes panic with "attempt to add with overflow".
Affected public APIs
Multi::wait()
Multi::poll() (feature poll_7_68_0)
Both call timeout_i32() to convert the user-supplied Duration.
To reproduce
use curl::multi::Multi;
use std::time::Duration;
fn main() {
let m = Multi::new();
// success: exactly i32::MAX milli secs
let ok = Duration::new(2_147_483, 647_999_999);
let r = m.wait(&mut [], ok);
println!(" ok: {:?}", r.is_ok()); // ok: true
// panic
let bad = Duration::new(2_147_483, 648_000_000);
let _ = m.wait(&mut [], bad); // panic inside timeout_i32()
}
Run with:
RUST_BACKTRACE=1 cargo run --example poc_timeout_overflow
Stack trace:
thread 'main' (340054) panicked at src/multi.rs:631:13:
attempt to add with overflow
stack backtrace:
0: __rustc::rust_begin_unwind
at /rustc/53732d5e076329a62f71d3c6901886ce8a71e812/library/std/src/panicking.rs:690:5
1: core::panicking::panic_fmt
at /rustc/53732d5e076329a62f71d3c6901886ce8a71e812/library/core/src/panicking.rs:80:14
2: core::panicking::panic_const::panic_const_add_overflow
at /rustc/53732d5e076329a62f71d3c6901886ce8a71e812/library/core/src/panicking.rs:175:17
3: <curl::multi::Multi>::timeout_i32
at ./src/multi.rs:631:13
4: <curl::multi::Multi>::wait
at ./src/multi.rs:611:26
5: poc_timeout_overflow::main
at ./examples/poc_timeout_overflow.rs:13:15
Test environment
- Version: curl-rust master (commit 0cfd9e3)
- OS: Ubuntu 24.04, x86_64
- Rustc: rustc 1.93.0-nightly (53732d5e0 2025-11-20)
Suggested fix
fn timeout_i32(timeout: Duration) -> i32 {
- let secs = timeout.as_secs();
- if secs > (i32::MAX / 1000) as u64 {
- // Duration too large, clamp at maximum value.
- i32::MAX
- } else {
- secs as i32 * 1000 + timeout.subsec_nanos() as i32 / 1_000_000
- }
+ timeout.as_millis().min(i32::MAX as u128) as i32
}
Describe the bug
Multi::wait()andMulti::poll()panic with "attempt to add with overflow" when called with a validDurationwithsecs == 2_147_483andnanos >= 648_000_000.The panic is caused by an off-by-one in the overflow guard inside
timeout_i32().Root cause
timeout_i32(src/multi.rs:625-633) is meant to convert aDurationto ani32millisecond count, clamping ati32::MAX:The guard uses
>instead of>=.i32::MAX / 1000 == 2_147_483, sosecs == 2_147_483passes through to the else branch.2_147_483 * 1000 == 2_147_483_000fits ini32, but whennanos >= 648_000_000,2_147_483_000 + 648 = 2_147_483_648 > i32::MAX (2_147_483_647), which overflows and causes panic with "attempt to add with overflow".Affected public APIs
Multi::wait()Multi::poll()(featurepoll_7_68_0)Both call
timeout_i32()to convert the user-suppliedDuration.To reproduce
Run with:
Stack trace:
Test environment
Suggested fix