fix: harden io-core and concurrency boundaries against panics (#4503)

fix(io-core,concurrency): harden boundary validation and arithmetic against panics (backlog#1024)
This commit is contained in:
Zhengchao An
2026-07-09 00:15:36 +08:00
committed by GitHub
parent 6cb47049e8
commit 20447422cf
7 changed files with 176 additions and 28 deletions
+95 -12
View File
@@ -117,27 +117,19 @@ impl ConcurrencyConfig {
let mut config = Self::default();
// Read from environment if available
if let Ok(val) = std::env::var("RUSTFS_TIMEOUT_DEFAULT")
&& let Ok(secs) = val.parse::<u64>()
{
if let Some(secs) = parse_env::<u64>("RUSTFS_TIMEOUT_DEFAULT") {
config.timeout_policy.default_timeout = Duration::from_secs(secs);
}
if let Ok(val) = std::env::var("RUSTFS_TIMEOUT_MAX")
&& let Ok(secs) = val.parse::<u64>()
{
if let Some(secs) = parse_env::<u64>("RUSTFS_TIMEOUT_MAX") {
config.timeout_policy.max_timeout = Duration::from_secs(secs);
}
if let Ok(val) = std::env::var("RUSTFS_BACKPRESSURE_BUFFER_SIZE")
&& let Ok(size) = val.parse::<usize>()
{
if let Some(size) = parse_env::<usize>("RUSTFS_BACKPRESSURE_BUFFER_SIZE") {
config.backpressure_policy.buffer_size = size;
}
if let Ok(val) = std::env::var("RUSTFS_IO_BUFFER_SIZE")
&& let Ok(size) = val.parse::<usize>()
{
if let Some(size) = parse_env::<usize>("RUSTFS_IO_BUFFER_SIZE") {
config.scheduler_policy.base_buffer_size = size;
}
@@ -146,6 +138,9 @@ impl ConcurrencyConfig {
/// Validate configuration
pub fn validate(&self) -> Result<(), ConfigError> {
if self.timeout_policy.default_timeout.is_zero() || self.timeout_policy.max_timeout.is_zero() {
return Err(ConfigError::InvalidTimeout("timeouts must be > 0".to_string()));
}
if self.timeout_policy.default_timeout > self.timeout_policy.max_timeout {
return Err(ConfigError::InvalidTimeout("default_timeout cannot exceed max_timeout".to_string()));
}
@@ -153,6 +148,10 @@ impl ConcurrencyConfig {
return Err(ConfigError::InvalidTimeout("min_timeout cannot exceed max_timeout".to_string()));
}
// A zero-capacity pipe (duplex(0)) never accepts writes, hanging producers forever.
if self.backpressure_policy.buffer_size == 0 {
return Err(ConfigError::InvalidBackpressure("buffer_size must be > 0".to_string()));
}
if self.backpressure_policy.high_watermark <= self.backpressure_policy.low_watermark
|| self.backpressure_policy.high_watermark > 100
{
@@ -161,16 +160,37 @@ impl ConcurrencyConfig {
));
}
if self.scheduler_policy.base_buffer_size == 0 {
return Err(ConfigError::InvalidScheduler("base_buffer_size must be > 0".to_string()));
}
if self.scheduler_policy.base_buffer_size > self.scheduler_policy.max_buffer_size {
return Err(ConfigError::InvalidScheduler(
"base_buffer_size cannot exceed max_buffer_size".to_string(),
));
}
// Ensure the derived io-core config also holds its invariants.
self.scheduler_policy
.to_core_config()
.validate()
.map_err(|e| ConfigError::InvalidScheduler(e.to_string()))?;
Ok(())
}
}
/// Parse an environment variable, logging a warning instead of silently
/// falling back to the default when the value is set but unparsable.
fn parse_env<T: std::str::FromStr>(name: &str) -> Option<T> {
let val = std::env::var(name).ok()?;
match val.parse() {
Ok(parsed) => Some(parsed),
Err(_) => {
tracing::warn!(env_var = name, value = %val, "ignoring unparsable environment variable");
None
}
}
}
/// Configuration error
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Clone, thiserror::Error)]
@@ -231,6 +251,69 @@ mod tests {
);
}
#[test]
fn test_zero_backpressure_buffer_size_rejected() {
let config = ConcurrencyConfig {
backpressure_policy: PipeBackpressurePolicy {
buffer_size: 0,
..Default::default()
},
..Default::default()
};
assert!(
matches!(config.validate(), Err(ConfigError::InvalidBackpressure(_))),
"validate() must reject buffer_size=0 (duplex(0) hangs all writes)"
);
}
#[test]
fn test_zero_timeouts_rejected() {
let config = ConcurrencyConfig {
timeout_policy: TimeoutManagerPolicy {
default_timeout: Duration::ZERO,
..Default::default()
},
..Default::default()
};
assert!(matches!(config.validate(), Err(ConfigError::InvalidTimeout(_))));
let config = ConcurrencyConfig {
timeout_policy: TimeoutManagerPolicy {
max_timeout: Duration::ZERO,
..Default::default()
},
..Default::default()
};
assert!(matches!(config.validate(), Err(ConfigError::InvalidTimeout(_))));
}
#[test]
fn test_zero_scheduler_base_buffer_size_rejected() {
let config = ConcurrencyConfig {
scheduler_policy: SchedulerPolicy {
base_buffer_size: 0,
..Default::default()
},
..Default::default()
};
assert!(matches!(config.validate(), Err(ConfigError::InvalidScheduler(_))));
}
#[test]
fn test_small_max_buffer_size_passes_core_validation() {
// max_buffer_size below the io-core default min_buffer_size (4KB) must
// still yield a valid core config (min lowered by to_core_config).
let config = ConcurrencyConfig {
scheduler_policy: SchedulerPolicy {
base_buffer_size: 1024,
max_buffer_size: 2048,
..Default::default()
},
..Default::default()
};
assert!(config.validate().is_ok());
}
#[test]
fn test_features() {
let features = ConcurrencyFeatures::all();
+15 -1
View File
@@ -49,12 +49,16 @@ impl Default for SchedulerPolicy {
impl SchedulerPolicy {
/// Convert facade policy to io-core scheduler config.
pub fn to_core_config(&self) -> rustfs_io_core::IoSchedulerConfig {
let default = rustfs_io_core::IoSchedulerConfig::default();
rustfs_io_core::IoSchedulerConfig {
base_buffer_size: self.base_buffer_size,
max_buffer_size: self.max_buffer_size,
// Keep min <= base <= max: the io-core default min (4KB) would make
// `clamp(min, max)` panic for policies with a smaller max_buffer_size.
min_buffer_size: default.min_buffer_size.min(self.base_buffer_size).min(self.max_buffer_size),
high_priority_size_threshold: self.high_priority_threshold,
low_priority_size_threshold: self.low_priority_threshold,
..rustfs_io_core::IoSchedulerConfig::default()
..default
}
}
}
@@ -266,6 +270,16 @@ mod tests {
assert!(priority.is_high());
}
#[test]
fn test_small_max_buffer_size_does_not_panic() {
// Regression: max_buffer_size below the io-core default min (4KB) used to
// panic in the core clamp (min > max) on the first buffer-size calculation.
let manager = SchedulerManager::new(1024, 2048, 512, 2048);
let size = manager.calculate_buffer_size(1024, StorageMedia::Ssd, AccessPattern::Sequential, IoLoadLevel::Medium, 1);
assert!(size > 0 && size <= 2048);
assert!(manager.core_config().validate().is_ok());
}
#[test]
fn test_io_strategy() {
let manager = SchedulerManager::new(1024, 4096, 512, 2048);