fix(ssh): per-channel buffer cap + enforce max_sessions (H7)
zhenyi committed
Modified dvcs/gitssh/handler/auth.rs dvcs/gitssh/handler/auth.rs +6 −0 View dvcs/gitssh/handler/auth.rs
@@ -74,6 +74,9 @@
7474 user: &str,
7575 public_key: &ssh_key::PublicKey,
7676 ) -> Result<Auth, russh::Error> {
77 if self.over_capacity {
78 return Ok(Auth::reject());
79 }
7780 if user.is_empty() || public_key.to_string().len() < 32 {
7881 return Ok(Auth::reject());
7982 }
@@ -92,6 +95,9 @@
9295 user: &str,
9396 public_key: &ssh_key::PublicKey,
9497 ) -> Result<Auth, russh::Error> {
98 if self.over_capacity {
99 return Ok(Auth::reject());
100 }
95101 if user.is_empty() || public_key.to_string().len() < 32 {
96102 return Ok(Auth::reject());
97103 }
Modified dvcs/gitssh/handler/channel.rs dvcs/gitssh/handler/channel.rs +36 −0 View dvcs/gitssh/handler/channel.rs
@@ -4,9 +4,32 @@
44
55use crate::handler::GitSshHandler;
66
7/// Per-channel buffering cap for a single SSH channel. Matches the HTTP-side
8/// `MAX_RPC_BODY` cap (`dvcs/gitsmart/handler/smart.rs`) so a malicious client
9/// cannot exhaust server memory by streaming bytes into a channel before
10/// EOF triggers the git spawn.
11pub const MAX_SSH_CHANNEL_BUF: usize = 1 << 30;
12
713impl GitSshHandler {
814 pub async fn data_impl(&mut self, channel: ChannelId, data: &[u8]) -> Result<(), russh::Error> {
915 let state = self.channels.entry(channel).or_default();
16 let projected = state
17 .buffer
18 .len()
19 .saturating_add(data.len());
20 if projected > MAX_SSH_CHANNEL_BUF {
21 tracing::warn!(
22 channel = ?channel,
23 buffered = state.buffer.len(),
24 incoming = data.len(),
25 limit = MAX_SSH_CHANNEL_BUF,
26 "ssh channel buffer overflow; dropping channel"
27 );
28 // Mark the channel as poisoned so the EOF handler refuses to
29 // spawn git, then ask russh to close the channel.
30 state.buffer_overflow = true;
31 return Err(russh::Error::Disconnect);
32 }
1033 state.buffer.extend_from_slice(data);
1134 Ok(())
1235 }
@@ -25,6 +48,7 @@
2548 let _ = session.flush();
2649 return Ok(());
2750 };
51 let overflowed = state.buffer_overflow;
2852 let op = state.git_op.take();
2953 let buffer = std::mem::take(&mut state.buffer);
3054 let repo_id = state.repo_id;
@@ -36,6 +60,18 @@
3660
3761 let handle = session.handle();
3862 tokio::spawn(async move {
63 if overflowed {
64 tracing::warn!(
65 channel = ?channel,
66 "refusing to spawn git for ssh channel that exceeded buffer cap"
67 );
68 // Surface the rejection to the client as a non-zero exit.
69 let _ = handle.exit_status_request(channel, 1).await;
70 let _ = handle.eof(channel).await;
71 let _ = handle.close(channel).await;
72 let _ = handle.data(channel, Bytes::from_static(b"remote: request rejected: ssh channel buffer cap exceeded\n")).await;
73 return;
74 }
3975 let is_write = op.as_ref().is_some_and(|o| o.is_write());
4076 if is_write
4177 && let Some(bare) = op.as_ref().map(|o| o.bare().to_path_buf())
Modified dvcs/gitssh/handler/mod.rs dvcs/gitssh/handler/mod.rs +8 −1 View dvcs/gitssh/handler/mod.rs
@@ -27,15 +27,20 @@
2727 pub repo_id: Option<Uuid>,
2828 pub channel_id: Option<Uuid>,
2929 pub actor_id: Option<Uuid>,
30 pub buffer_overflow: bool,
3031}
3132
3233pub struct GitSshHandler {
3334 pub service: Arc<GitSshService>,
3435 pub client_addr: Option<SocketAddr>,
35 #[allow(dead_code)]
3636 pub session_permit: Option<OwnedSemaphorePermit>,
3737 pub operator: Option<users::Model>,
3838 pub channels: HashMap<ChannelId, ChannelState>,
39 /// True when this handler was admitted by `try_acquire_owned` returning
40 /// `None` — i.e. the SSH session limit is already saturated. The handler
41 /// rejects every authentication attempt so the client disconnects
42 /// quickly without consuming a real session slot.
43 pub over_capacity: bool,
3944}
4045
4146impl GitSshHandler {
@@ -44,12 +49,14 @@
4449 client_addr: Option<SocketAddr>,
4550 session_permit: Option<OwnedSemaphorePermit>,
4651 ) -> Self {
52 let over_capacity = session_permit.is_none();
4753 Self {
4854 service,
4955 client_addr,
5056 session_permit,
5157 operator: None,
5258 channels: HashMap::new(),
59 over_capacity,
5360 }
5461 }
5562