Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion crates/openshell-cli/src/commands/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,8 @@ pub fn parse_duration_to_ms(s: &str) -> Result<i64> {
));
}
};
Ok(num * multiplier)
num.checked_mul(multiplier)
.ok_or_else(|| miette::miette!("duration value is too large: {s}"))
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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"));
}
}
2 changes: 1 addition & 1 deletion crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down
20 changes: 18 additions & 2 deletions crates/openshell-driver-docker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2689,7 +2689,15 @@ fn parse_cpu_limit(value: &str) -> Result<Option<i64>, 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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gator-agent

Warning (CWE-190): i64::MAX as f64 rounds to 2^63, so an intermediate equal to 2^63 passes this > check and the cast silently saturates to i64::MAX. Reject this boundary (for example with >=) or use an exact checked conversion, and add a 2^63 regression test.

return Err(Status::failed_precondition(format!(
"docker cpu_limit '{value}' is too large",
)));
}

Ok(Some(nano_cpus as i64))
}

#[allow(clippy::cast_possible_truncation)]
Expand Down Expand Up @@ -2735,7 +2743,15 @@ fn parse_memory_limit(value: &str) -> Result<Option<i64>, 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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gator-agent

Warning (CWE-190): The same 2^63 boundary issue applies here: a quantity producing exactly 2^63 bytes passes validation and clamps to i64::MAX. Reject equality or use an exact checked conversion, with a boundary regression test.

return Err(Status::failed_precondition(format!(
"docker memory_limit '{value}' is too large",
)));
}

Ok(Some(bytes as i64))
}

fn sandbox_from_container_summary(
Expand Down
14 changes: 14 additions & 0 deletions crates/openshell-driver-docker/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
17 changes: 15 additions & 2 deletions crates/openshell-driver-podman/src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gator-agent

Warning (CWE-190): saturating_mul converts overflow to u64::MAX, but Podman’s inherited health-check interval uses a signed Go time.Duration in nanoseconds. Invalid configuration can therefore survive startup and fail later during sandbox creation. Validate the interval against i64::MAX / 1_000_000_000 during driver initialization, use checked multiplication, and add a boundary test.

timeout: 2_000_000_000,
retries: 10,
start_period: 5_000_000_000,
Expand Down Expand Up @@ -1107,8 +1109,13 @@ fn parse_cpu_to_microseconds(quantity: &str) -> Option<u64> {
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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gator-agent

Warning (CWE-190): u64::MAX as f64 rounds to 2^64, so an intermediate equal to 2^64 passes this check and the cast saturates to u64::MAX. Reject this boundary (for example with >=) or use checked exact parsing, and test the 2^64 boundary.

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.
Expand Down Expand Up @@ -1183,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));
Expand Down
5 changes: 4 additions & 1 deletion crates/openshell-server/src/grpc/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down
33 changes: 30 additions & 3 deletions crates/openshell-tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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), "-");
}
}
Loading