From 4290c65f1fdaed5160778ac85e4b64232883dcad Mon Sep 17 00:00:00 2001 From: GatewayJ <18332154+GatewayJ@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:30:56 +0800 Subject: [PATCH] fix(console): prevent pool volume count overflow --- src/console/handlers/pools.rs | 7 +++++-- src/console/models/pool.rs | 27 ++++++++++++++++++++++++++- src/console/openapi.rs | 17 +++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/console/handlers/pools.rs b/src/console/handlers/pools.rs index 0ff4aea..c07df1c 100755 --- a/src/console/handlers/pools.rs +++ b/src/console/handlers/pools.rs @@ -511,7 +511,10 @@ pub async fn list_pools( name: pool.name.clone(), servers: pool.servers, volumes_per_server: pool.persistence.volumes_per_server, - total_volumes: pool.servers * pool.persistence.volumes_per_server, + total_volumes: PoolDetails::total_volumes( + pool.servers, + pool.persistence.volumes_per_server, + ), storage_class, volume_size, replicas, @@ -575,7 +578,7 @@ pub async fn add_pool( ), }); } - let total_volumes = req.servers.saturating_mul(req.volumes_per_server); + let total_volumes = PoolDetails::total_volumes(req.servers, req.volumes_per_server); // Build Pool spec let new_pool = Pool { diff --git a/src/console/models/pool.rs b/src/console/models/pool.rs index 6e3cdfa..a8bbbad 100755 --- a/src/console/models/pool.rs +++ b/src/console/models/pool.rs @@ -21,7 +21,7 @@ pub struct PoolDetails { pub name: String, pub servers: i32, pub volumes_per_server: i32, - pub total_volumes: i32, + pub total_volumes: i64, pub storage_class: Option, pub volume_size: Option, pub replicas: i32, @@ -43,6 +43,17 @@ pub struct PoolDetails { pub created_at: Option, } +impl PoolDetails { + /// Return the exact volume count derived from the CRD's `i32` pool dimensions. + /// + /// Widening both operands before multiplication is lossless because every `i32 * i32` + /// product fits in an `i64`. This preserves the real count instead of rejecting or clamping a + /// valid CRD value. + pub(crate) fn total_volumes(servers: i32, volumes_per_server: i32) -> i64 { + i64::from(servers) * i64::from(volumes_per_server) + } +} + /// Response listing pools for a tenant #[derive(Debug, Serialize, ToSchema)] pub struct PoolListResponse { @@ -120,3 +131,17 @@ pub struct PoolDecommissionRequestResponse { pub request_id: String, pub action: String, } + +#[cfg(test)] +mod tests { + use super::PoolDetails; + + #[test] + fn total_volumes_widens_before_multiplication() { + assert_eq!(PoolDetails::total_volumes(i32::MAX, 2), 4_294_967_294); + assert_eq!( + PoolDetails::total_volumes(i32::MAX, i32::MAX), + 4_611_686_014_132_420_609 + ); + } +} diff --git a/src/console/openapi.rs b/src/console/openapi.rs index 693d1dc..c8340fc 100644 --- a/src/console/openapi.rs +++ b/src/console/openapi.rs @@ -736,6 +736,23 @@ mod tests { } } + #[test] + fn pool_total_volumes_documents_widened_integer() { + let spec = serde_json::to_value(ApiDoc::openapi()).expect("OpenAPI spec serializes"); + let total_volumes = spec + .pointer("/components/schemas/PoolDetails/properties/total_volumes") + .expect("PoolDetails.total_volumes schema exists"); + + assert_eq!( + total_volumes.get("type").and_then(Value::as_str), + Some("integer") + ); + assert_eq!( + total_volumes.get("format").and_then(Value::as_str), + Some("int64") + ); + } + #[test] fn tenant_api_documents_provisioning_fields() { let spec = serde_json::to_value(ApiDoc::openapi()).expect("OpenAPI spec serializes");