mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 15:37:02 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user