From 60a26000c642c62031367cf6f5b3f96efff56858 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 24 Jul 2026 05:28:49 -0500 Subject: [PATCH 1/7] fix(server): avoid i64 overflow in SSH session TTL The SSH session expiry calculation cast a u64 TTL to i64 and multiplied by 1000 without bounds checking. A configured TTL larger than i64::MAX / 1000 overflows and panics in debug builds. Use i64::try_from with saturating_mul and saturating_add so any out-of-range TTL clamps to i64::MAX milliseconds. Signed-off-by: Andrew White --- crates/openshell-server/src/grpc/sandbox.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 57bf421b88..b5a604228d 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -1421,7 +1421,10 @@ pub(super) async fn handle_create_ssh_session( let token = uuid::Uuid::new_v4().to_string(); let now_ms = current_time_ms(); let expires_at_ms = if state.config.ssh_session_ttl_secs > 0 { - now_ms + (state.config.ssh_session_ttl_secs as i64 * 1000) + let ttl_ms = i64::try_from(state.config.ssh_session_ttl_secs) + .unwrap_or(i64::MAX) + .saturating_mul(1000); + now_ms.saturating_add(ttl_ms) } else { 0 }; From 9eeae6c16cf2981aeab8096f5457c56345c4abc0 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 24 Jul 2026 05:28:49 -0500 Subject: [PATCH 2/7] fix(driver-podman): avoid u64 overflow in health-check interval The Podman health-check interval multiplied a u64 seconds value by 1_000_000_000 to produce nanoseconds. A configured interval above u64::MAX / 1e9 overflows. Use saturating_mul so huge values clamp to u64::MAX nanoseconds instead of panicking. Signed-off-by: Andrew White --- crates/openshell-driver-podman/src/container.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/openshell-driver-podman/src/container.rs b/crates/openshell-driver-podman/src/container.rs index 4ca2575d82..90976c1057 100644 --- a/crates/openshell-driver-podman/src/container.rs +++ b/crates/openshell-driver-podman/src/container.rs @@ -987,7 +987,9 @@ pub fn build_container_spec_with_token_and_gpu_devices( openshell_core::config::DEFAULT_SSH_PORT ), ], - interval: config.health_check_interval_secs * 1_000_000_000, + interval: config + .health_check_interval_secs + .saturating_mul(1_000_000_000), timeout: 2_000_000_000, retries: 10, start_period: 5_000_000_000, From 63de26da63cb7cef8d6afdbc4db194b0a74cf77f Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 24 Jul 2026 05:28:49 -0500 Subject: [PATCH 3/7] fix(cli): reject overflow in --since duration values parse_duration_to_ms parsed the numeric part as i64 and multiplied it by a fixed multiplier, which can overflow for large user inputs such as '100000000000000h'. Use checked_mul and return a clear error instead of panicking. Add a regression test. Signed-off-by: Andrew White --- crates/openshell-cli/src/commands/common.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/openshell-cli/src/commands/common.rs b/crates/openshell-cli/src/commands/common.rs index 7fa5cd1fec..b78cea533e 100644 --- a/crates/openshell-cli/src/commands/common.rs +++ b/crates/openshell-cli/src/commands/common.rs @@ -733,7 +733,8 @@ pub fn parse_duration_to_ms(s: &str) -> Result { )); } }; - Ok(num * multiplier) + num.checked_mul(multiplier) + .ok_or_else(|| miette::miette!("duration value is too large: {s}")) } // --------------------------------------------------------------------------- @@ -948,3 +949,18 @@ pub fn scrub_git_env(command: &mut Command) -> &mut Command { } command } + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_duration_to_ms_rejects_overflow() { + let err = parse_duration_to_ms("100000000000000h").expect_err("overflow should error"); + assert!(err.to_string().contains("too large")); + } +} From bc774569724056548038e22b5df85311455381d2 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 24 Jul 2026 05:28:49 -0500 Subject: [PATCH 4/7] fix(cli): avoid underflow computing --since log start timestamp After parsing a duration, the CLI subtracted it from the current Unix epoch milliseconds. A duration larger than now_ms underflows. Use saturating_sub so the start timestamp clamps to 0 instead of panicking in debug builds. Signed-off-by: Andrew White --- crates/openshell-cli/src/run.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index a2ad725a4c..fb7349c6cf 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -7699,7 +7699,7 @@ pub async fn sandbox_logs( .as_millis(), ) .into_diagnostic()?; - now_ms - dur_ms + now_ms.saturating_sub(dur_ms) } else { 0 }; From a7687efc9b3c7df95360e4e5008ed8b826fa01bd Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 24 Jul 2026 05:28:49 -0500 Subject: [PATCH 5/7] fix(tui): avoid underflow formatting future ages format_age subtracted created_secs from now before checking whether the timestamp was in the future, so a future or skewed timestamp underflowed before the guard could return '-'. Check created_secs > now before subtracting, and add regression tests for future, zero, and negative timestamps. Signed-off-by: Andrew White --- crates/openshell-tui/src/lib.rs | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index b3937e2b90..cd86931781 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -2603,11 +2603,10 @@ fn format_age(epoch_ms: i64) -> String { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map_or(0, |d| d.as_secs().cast_signed()); - let diff = now - created_secs; - if diff < 0 { + if created_secs > now { return String::from("-"); } - let diff = diff.cast_unsigned(); + let diff = (now - created_secs).cast_unsigned(); if diff < 60 { format!("{diff}s") } else if diff < 3600 { @@ -2649,3 +2648,31 @@ fn days_to_ymd(days: i64) -> (i64, i64, i64) { let y = if m <= 2 { y + 1 } else { y }; (y, m, d) } + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn format_age_handles_future_timestamp() { + let future_ms = i64::try_from( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(), + ) + .unwrap() + + 10_000; + assert_eq!(format_age(future_ms), "-"); + } + + #[test] + fn format_age_handles_zero_and_negative() { + assert_eq!(format_age(0), "-"); + assert_eq!(format_age(-1), "-"); + } +} From fb7090d5f0ac63c46cb4f79a91d122816d56b7f7 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 24 Jul 2026 05:42:11 -0500 Subject: [PATCH 6/7] fix(driver-podman): avoid f64 overflow in CPU limit parse_cpu_to_microseconds checked that the input cores value was finite, but the product cores * 100_000 could still overflow to infinity (e.g. '1e300'). An infinite product cast to u64 saturates to u64::MAX, producing a bogus quota. Reject the value when the product is non-finite or exceeds u64::MAX. Add a regression test. Signed-off-by: Andrew White --- crates/openshell-driver-podman/src/container.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/openshell-driver-podman/src/container.rs b/crates/openshell-driver-podman/src/container.rs index 90976c1057..881fa65525 100644 --- a/crates/openshell-driver-podman/src/container.rs +++ b/crates/openshell-driver-podman/src/container.rs @@ -1109,8 +1109,13 @@ fn parse_cpu_to_microseconds(quantity: &str) -> Option { if cores <= 0.0 || cores.is_nan() || cores.is_infinite() { return None; } + let micros_f = cores * 100_000.0; + #[allow(clippy::cast_precision_loss)] + if !micros_f.is_finite() || micros_f > u64::MAX as f64 { + return None; + } #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - let val = (cores * 100_000.0) as u64; + let val = micros_f as u64; val }; // A quota of 0 microseconds is invalid — treat as no limit. @@ -1185,6 +1190,12 @@ mod tests { assert_eq!(parse_cpu_to_microseconds("0.5"), Some(50_000)); } + #[test] + fn parse_cpu_huge_value_returns_none_instead_of_overflow() { + // A finite f64 whose product with 100_000 overflows to infinity. + assert_eq!(parse_cpu_to_microseconds("1e300"), None); + } + #[test] fn parse_memory_binary_suffixes() { assert_eq!(parse_memory_to_bytes("256Mi"), Some(256 * 1024 * 1024)); From 260664b542b829e612093b7ed7e617192daed804 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 24 Jul 2026 05:42:11 -0500 Subject: [PATCH 7/7] fix(driver-docker): avoid f64 overflow in CPU and memory limits parse_cpu_limit and parse_memory_limit parsed user-supplied f64 values and multiplied them by large constants before casting to i64. A huge input could make the product infinite, silently saturating to i64::MAX. Check that the rounded product is finite and within i64 range before casting, and return a clear failed-precondition error. Add regression tests. Signed-off-by: Andrew White --- crates/openshell-driver-docker/src/lib.rs | 20 ++++++++++++++++++-- crates/openshell-driver-docker/src/tests.rs | 14 ++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/crates/openshell-driver-docker/src/lib.rs b/crates/openshell-driver-docker/src/lib.rs index 2f89c2229e..fd9dcaec1f 100644 --- a/crates/openshell-driver-docker/src/lib.rs +++ b/crates/openshell-driver-docker/src/lib.rs @@ -2689,7 +2689,15 @@ fn parse_cpu_limit(value: &str) -> Result, Status> { )); } - Ok(Some((cores * 1_000_000_000.0).round() as i64)) + let nano_cpus = (cores * 1_000_000_000.0).round(); + #[allow(clippy::cast_precision_loss)] + if !nano_cpus.is_finite() || nano_cpus < i64::MIN as f64 || nano_cpus > i64::MAX as f64 { + return Err(Status::failed_precondition(format!( + "docker cpu_limit '{value}' is too large", + ))); + } + + Ok(Some(nano_cpus as i64)) } #[allow(clippy::cast_possible_truncation)] @@ -2735,7 +2743,15 @@ fn parse_memory_limit(value: &str) -> Result, Status> { } }; - Ok(Some((amount * multiplier).round() as i64)) + let bytes = (amount * multiplier).round(); + #[allow(clippy::cast_precision_loss)] + if !bytes.is_finite() || bytes < i64::MIN as f64 || bytes > i64::MAX as f64 { + return Err(Status::failed_precondition(format!( + "docker memory_limit '{value}' is too large", + ))); + } + + Ok(Some(bytes as i64)) } fn sandbox_from_container_summary( diff --git a/crates/openshell-driver-docker/src/tests.rs b/crates/openshell-driver-docker/src/tests.rs index 67036b2192..74c1ae66c9 100644 --- a/crates/openshell-driver-docker/src/tests.rs +++ b/crates/openshell-driver-docker/src/tests.rs @@ -2226,3 +2226,17 @@ fn container_state_needs_resume_matches_startable_states() { ); } } + +#[test] +fn parse_cpu_limit_rejects_overflow() { + let err = parse_cpu_limit("1e300").unwrap_err(); + assert!(err.message().contains("too large")); +} + +#[test] +fn parse_memory_limit_rejects_overflow() { + // 308 nines is finite as f64 (~1e308), but multiplying by Gi overflows to inf. + let huge = "9".repeat(308) + "Gi"; + let err = parse_memory_limit(&huge).unwrap_err(); + assert!(err.message().contains("too large")); +}