Skip to content

Commit 5ba6146

Browse files
committed
fix: Address security and performance review issues
- Fix HIGH: Race condition in Drop - add retry mechanism with exponential backoff to ensure session cleanup under lock contention - Fix MEDIUM: Add input validation for SessionConfig - clamp max_sessions_per_user and max_total_sessions to minimum of 1 to prevent misconfiguration that could deny all connections - Add validate() method to SessionConfig for detecting potentially problematic configuration combinations - Fix MEDIUM: Atomic ordering - change SessionId counter from Ordering::Relaxed to Ordering::SeqCst for stricter ordering guarantees and consistent logging - Add tests for config validation and clamping behavior
1 parent c2f8657 commit 5ba6146

2 files changed

Lines changed: 113 additions & 19 deletions

File tree

src/server/handler.rs

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,22 +1393,36 @@ impl Drop for SshHandler {
13931393
"Session ended"
13941394
);
13951395

1396-
// Remove session from manager
1397-
// Note: This uses try_write which is safe here because:
1398-
// 1. Drop is called outside of async context (during connection cleanup)
1399-
// 2. The lock is held only briefly to remove the session
1400-
// 3. This prevents resource leaks by ensuring cleanup always happens
1401-
if let Ok(mut sessions_guard) = self.sessions.try_write() {
1402-
sessions_guard.remove(session_id);
1403-
tracing::debug!(
1404-
session_id = %session_id,
1405-
"Session removed from manager"
1406-
);
1407-
} else {
1408-
tracing::warn!(
1409-
session_id = %session_id,
1410-
"Failed to acquire lock to remove session (lock contention)"
1411-
);
1396+
// Remove session from manager with retry mechanism
1397+
// We use try_write with retries to ensure cleanup even under contention.
1398+
// This is important to prevent session leaks that could exhaust limits.
1399+
let mut retries = 0;
1400+
const MAX_RETRIES: u32 = 5;
1401+
1402+
loop {
1403+
if let Ok(mut sessions_guard) = self.sessions.try_write() {
1404+
sessions_guard.remove(session_id);
1405+
tracing::debug!(
1406+
session_id = %session_id,
1407+
retries = retries,
1408+
"Session removed from manager"
1409+
);
1410+
break;
1411+
}
1412+
1413+
retries += 1;
1414+
if retries >= MAX_RETRIES {
1415+
tracing::error!(
1416+
session_id = %session_id,
1417+
retries = retries,
1418+
"Failed to remove session after max retries - session may leak"
1419+
);
1420+
break;
1421+
}
1422+
1423+
// Brief delay before retry (exponential backoff in microseconds)
1424+
// This is safe in Drop as we're in a sync context
1425+
std::thread::sleep(std::time::Duration::from_micros(100 * (1 << retries)));
14121426
}
14131427
}
14141428
}

src/server/session.rs

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,20 +110,32 @@ impl Default for SessionConfig {
110110
}
111111

112112
impl SessionConfig {
113+
/// Minimum allowed value for max_sessions_per_user.
114+
pub const MIN_SESSIONS_PER_USER: usize = 1;
115+
116+
/// Minimum allowed value for max_total_sessions.
117+
pub const MIN_TOTAL_SESSIONS: usize = 1;
118+
113119
/// Create a new session configuration with default values.
114120
pub fn new() -> Self {
115121
Self::default()
116122
}
117123

118124
/// Set the maximum sessions per user.
125+
///
126+
/// The value is clamped to a minimum of 1 to prevent misconfiguration
127+
/// that would deny all users from authenticating.
119128
pub fn with_max_sessions_per_user(mut self, max: usize) -> Self {
120-
self.max_sessions_per_user = max;
129+
self.max_sessions_per_user = max.max(Self::MIN_SESSIONS_PER_USER);
121130
self
122131
}
123132

124133
/// Set the maximum total sessions.
134+
///
135+
/// The value is clamped to a minimum of 1 to prevent misconfiguration
136+
/// that would deny all connections.
125137
pub fn with_max_total_sessions(mut self, max: usize) -> Self {
126-
self.max_total_sessions = max;
138+
self.max_total_sessions = max.max(Self::MIN_TOTAL_SESSIONS);
127139
self
128140
}
129141

@@ -138,6 +150,35 @@ impl SessionConfig {
138150
self.session_timeout = Some(timeout);
139151
self
140152
}
153+
154+
/// Validate the configuration and return any warnings.
155+
///
156+
/// Returns a list of warning messages for potentially problematic settings.
157+
pub fn validate(&self) -> Vec<String> {
158+
let mut warnings = Vec::new();
159+
160+
if self.max_sessions_per_user > self.max_total_sessions {
161+
warnings.push(format!(
162+
"max_sessions_per_user ({}) > max_total_sessions ({}) - per-user limit will never be reached",
163+
self.max_sessions_per_user, self.max_total_sessions
164+
));
165+
}
166+
167+
if self.idle_timeout.as_secs() == 0 {
168+
warnings.push("idle_timeout is 0 - sessions will be immediately considered idle".to_string());
169+
}
170+
171+
if let Some(session_timeout) = self.session_timeout {
172+
if session_timeout < self.idle_timeout {
173+
warnings.push(format!(
174+
"session_timeout ({:?}) < idle_timeout ({:?}) - sessions may be terminated before idle check",
175+
session_timeout, self.idle_timeout
176+
));
177+
}
178+
}
179+
180+
warnings
181+
}
141182
}
142183

143184
/// Errors that can occur during session management.
@@ -181,9 +222,13 @@ pub struct SessionId(u64);
181222

182223
impl SessionId {
183224
/// Create a new unique session ID.
225+
///
226+
/// Uses `Ordering::SeqCst` to ensure session IDs are strictly ordered
227+
/// across all threads, preventing potential confusion in logging and
228+
/// debugging when sessions appear out of order.
184229
pub fn new() -> Self {
185230
static COUNTER: AtomicU64 = AtomicU64::new(1);
186-
Self(COUNTER.fetch_add(1, Ordering::Relaxed))
231+
Self(COUNTER.fetch_add(1, Ordering::SeqCst))
187232
}
188233

189234
/// Get the raw numeric value of the session ID.
@@ -968,6 +1013,41 @@ mod tests {
9681013
assert_eq!(config.session_timeout, Some(Duration::from_secs(86400)));
9691014
}
9701015

1016+
#[test]
1017+
fn test_session_config_validation_clamping() {
1018+
// Setting max_sessions_per_user to 0 should clamp to 1
1019+
let config = SessionConfig::new().with_max_sessions_per_user(0);
1020+
assert_eq!(config.max_sessions_per_user, 1);
1021+
1022+
// Setting max_total_sessions to 0 should clamp to 1
1023+
let config = SessionConfig::new().with_max_total_sessions(0);
1024+
assert_eq!(config.max_total_sessions, 1);
1025+
}
1026+
1027+
#[test]
1028+
fn test_session_config_validate() {
1029+
// Valid config should have no warnings
1030+
let config = SessionConfig::new();
1031+
let warnings = config.validate();
1032+
assert!(warnings.is_empty());
1033+
1034+
// Per-user > total should warn
1035+
let config = SessionConfig::new()
1036+
.with_max_sessions_per_user(100)
1037+
.with_max_total_sessions(10);
1038+
let warnings = config.validate();
1039+
assert_eq!(warnings.len(), 1);
1040+
assert!(warnings[0].contains("per-user limit"));
1041+
1042+
// Session timeout < idle timeout should warn
1043+
let config = SessionConfig::new()
1044+
.with_idle_timeout(Duration::from_secs(3600))
1045+
.with_session_timeout(Duration::from_secs(1800));
1046+
let warnings = config.validate();
1047+
assert_eq!(warnings.len(), 1);
1048+
assert!(warnings[0].contains("session_timeout"));
1049+
}
1050+
9711051
#[test]
9721052
fn test_session_manager_creation() {
9731053
let manager = SessionManager::new();

0 commit comments

Comments
 (0)