-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix: assorted numeric overflow fixes #2462
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
60a2600
9eeae6c
63de26d
bc77456
a7687ef
fb7090d
260664b
04139eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2226,3 +2226,45 @@ 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")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parse_cpu_limit_rejects_i64_max_boundary() { | ||
| // 9_223_372_037 cores * 1e9 rounds to 2^63, which used to pass the > check | ||
| // and silently saturate to i64::MAX. It must now be rejected. | ||
| let err = parse_cpu_limit("9223372037").unwrap_err(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Warning: This does not test the equality boundary: |
||
| assert!(err.message().contains("too large")); | ||
|
|
||
| // One core below the boundary is still valid. | ||
| assert_eq!( | ||
| parse_cpu_limit("9223372036").unwrap(), | ||
| Some(9_223_372_036_000_000_000) | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parse_memory_limit_rejects_i64_max_boundary() { | ||
| // 8192 PiB = 2^63 bytes, which used to pass the > check and silently | ||
| // saturate to i64::MAX. It must now be rejected. | ||
| let err = parse_memory_limit("8192Pi").unwrap_err(); | ||
| assert!(err.message().contains("too large")); | ||
|
|
||
| // One PiB below the boundary is still valid. | ||
| assert_eq!( | ||
| parse_memory_limit("8191Pi").unwrap(), | ||
| Some(8191 * 1024_i64.pow(5)) | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -987,7 +987,16 @@ 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 | ||
| .checked_mul(1_000_000_000) | ||
| .filter(|ns| *ns <= i64::MAX as u64) | ||
| .ok_or_else(|| { | ||
| ComputeDriverError::InvalidArgument(format!( | ||
| "health_check_interval_secs {} exceeds maximum allowed nanoseconds", | ||
| config.health_check_interval_secs | ||
| )) | ||
| })?, | ||
| timeout: 2_000_000_000, | ||
| retries: 10, | ||
| start_period: 5_000_000_000, | ||
|
|
@@ -1107,8 +1116,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 { | ||
| 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. | ||
|
|
@@ -1183,6 +1197,41 @@ 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_cpu_rejects_u64_max_boundary() { | ||
| // 184_467_440_737_095_520 cores * 100_000 rounds to 2^64, which used | ||
| // to pass the > check and silently saturate to u64::MAX. It must now | ||
| // be rejected. | ||
| assert_eq!(parse_cpu_to_microseconds("184467440737095520"), None); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Warning: This input is approximately 1,000 times the |
||
|
|
||
| // One core below the boundary is still valid. | ||
| assert_eq!( | ||
| parse_cpu_to_microseconds("184467440737095"), | ||
| Some(18_446_744_073_709_500_416) | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn container_spec_rejects_health_check_interval_overflow() { | ||
| let sandbox = test_sandbox("test-id", "test-name"); | ||
| let mut config = test_config(); | ||
| // i64::MAX nanoseconds / 1_000_000_000 ns/s = 9_223_372_036 seconds. | ||
| // One second over must be rejected before it can saturate. | ||
| config.health_check_interval_secs = 9_223_372_037; | ||
| let err = try_build_container_spec_with_token(&sandbox, &config, None).unwrap_err(); | ||
| assert!( | ||
| matches!(err, ComputeDriverError::InvalidArgument(_)), | ||
| "expected InvalidArgument, got {err:?}" | ||
| ); | ||
| assert!(format!("{err}").contains("health_check_interval_secs")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parse_memory_binary_suffixes() { | ||
| assert_eq!(parse_memory_to_bytes("256Mi"), Some(256 * 1024 * 1024)); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Warning (CWE-20/CWE-613): Saturating an unrepresentable TTL to |
||
| .unwrap_or(i64::MAX) | ||
| .saturating_mul(1000); | ||
| now_ms.saturating_add(ttl_ms) | ||
| } else { | ||
| 0 | ||
| }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Warning:
saturating_subdoes not clamp a duration older than the Unix epoch to zero; it returns a negative timestamp whenever the subtraction remains representable. Negative durations are also accepted and produce future timestamps. Reject negative durations, then clamp an overlong duration to zero (for example,now_ms.checked_sub(dur_ms).unwrap_or(0).max(0)) and add regression tests for both cases.