From 01ff663e5d877059665410aa9c5c28ca00036432 Mon Sep 17 00:00:00 2001 From: Ryan Whitworth Date: Sat, 28 Mar 2026 15:52:45 -0400 Subject: [PATCH] fix(server): eliminate nested mutex acquisition in SSH tunnel handler Restructure the per-sandbox connection limit check to release ssh_connections_by_sandbox before acquiring ssh_connections_by_token for the rollback. The previous code held both mutexes simultaneously, creating a potential deadlock if any other code path acquired them in the opposite order. --- crates/openshell-server/src/ssh_tunnel.rs | 29 ++++++++++++++--------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/crates/openshell-server/src/ssh_tunnel.rs b/crates/openshell-server/src/ssh_tunnel.rs index 5dbff9b5..739339c8 100644 --- a/crates/openshell-server/src/ssh_tunnel.rs +++ b/crates/openshell-server/src/ssh_tunnel.rs @@ -123,22 +123,29 @@ async fn ssh_connect( } // Enforce per-sandbox concurrent connection limit. - { + let sandbox_over_limit = { let mut counts = state.ssh_connections_by_sandbox.lock().unwrap(); let count = counts.entry(sandbox_id.clone()).or_insert(0); if *count >= MAX_CONNECTIONS_PER_SANDBOX { - // Roll back the per-token increment. - let mut token_counts = state.ssh_connections_by_token.lock().unwrap(); - if let Some(c) = token_counts.get_mut(&token) { - *c = c.saturating_sub(1); - if *c == 0 { - token_counts.remove(&token); - } + true + } else { + *count += 1; + false + } + }; + // Lock is released here before any rollback — avoids nested mutex acquisition. + + if sandbox_over_limit { + // Roll back the per-token increment — no nested locks. + let mut token_counts = state.ssh_connections_by_token.lock().unwrap(); + if let Some(c) = token_counts.get_mut(&token) { + *c = c.saturating_sub(1); + if *c == 0 { + token_counts.remove(&token); } - warn!(sandbox_id = %sandbox_id, "SSH tunnel: per-sandbox connection limit reached"); - return StatusCode::TOO_MANY_REQUESTS.into_response(); } - *count += 1; + warn!(sandbox_id = %sandbox_id, "SSH tunnel: per-sandbox connection limit reached"); + return StatusCode::TOO_MANY_REQUESTS.into_response(); } let handshake_secret = state.config.ssh_handshake_secret.clone();