From: Javier Sagredo Date: Sun, 31 May 2026 23:40:01 +0000 (+0200) Subject: tls: release connection state on close and cache ServerConfig X-Git-Url: https://git.sagredo.dev/?a=commitdiff_plain;h=52d70bcb6abea4157746375ce26741fe63b0db82;p=scryer-prolog.git tls: release connection state on close and cache ServerConfig Closing a TLS or TCP stream previously only shut down the socket / sent close_notify without dropping the arena-allocated payload, so the rustls ServerConnection (and its buffers) lingered until the arena was garbage-collected. For a server negotiating a fresh TLS connection per request, this caused resident memory to climb steadily. Call drop_payload() in Stream::close for the NamedTcp and NamedTls variants so the underlying resources are freed immediately, matching the file, byte and pipe stream variants. Additionally, tls_accept_client rebuilt a ServerConfig on every connection, re-parsing the certificate chain and private key each time. Cache the most recently built config keyed by the raw cert/key bytes and reuse it, avoiding the repeated parse and allocation. Co-Authored-By: Claude Opus 4.8 (1M context) --- diff --git a/src/machine/streams.rs b/src/machine/streams.rs index 3f6adc38..207b6cff 100644 --- a/src/machine/streams.rs +++ b/src/machine/streams.rs @@ -1586,13 +1586,22 @@ impl Stream { pub(crate) fn close(&mut self) -> Result<(), std::io::Error> { match self { Stream::NamedTcp(ref mut tcp_stream) => { - tcp_stream.inner_mut().tcp_stream.shutdown(Shutdown::Both) + let result = tcp_stream.inner_mut().tcp_stream.shutdown(Shutdown::Both); + // Release the underlying socket immediately instead of waiting + // for the arena to be garbage-collected. + tcp_stream.drop_payload(); + result } #[cfg(feature = "tls")] Stream::NamedTls(ref mut tls_stream) => { let inner = tls_stream.inner_mut(); inner.tls_stream.send_close_notify(); - inner.tls_stream.flush() + let result = inner.tls_stream.flush(); + // Release the rustls connection state (buffers, ServerConfig) + // immediately instead of waiting for the arena to be + // garbage-collected. + tls_stream.drop_payload(); + result } #[cfg(feature = "http")] Stream::HttpRead(ref mut http_stream) => { diff --git a/src/machine/system_calls.rs b/src/machine/system_calls.rs index ffd7a29f..8dba07c7 100644 --- a/src/machine/system_calls.rs +++ b/src/machine/system_calls.rs @@ -7293,32 +7293,19 @@ impl Machine { #[cfg(feature = "tls")] #[inline(always)] pub(crate) fn tls_accept_client(&mut self) -> CallResult { - let cert_pem = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet")); - let key_pem = self.string_encoding_bytes(self.machine_st.registers[2], atom!("octet")); + use std::cell::RefCell; - let cert_chain: Vec> = - match rustls_pemfile::certs(&mut &cert_pem[..]).collect::, _>>() { - Ok(certs) if !certs.is_empty() => certs, - _ => { - return Err(self.machine_st.open_permission_error( - self.machine_st.registers[1], - atom!("tls_server_negotiate"), - 3, - )); - } - }; + // A new TLS connection is negotiated for every incoming request. Building + // a fresh ServerConfig each time means re-parsing the certificate chain + // and (potentially large) private key on every connection. Cache the most + // recently built config, keyed by the raw cert/key bytes, and reuse it. + thread_local! { + static CONFIG_CACHE: RefCell, Vec, Arc)>> = + RefCell::new(None); + } - let private_key: PrivateKeyDer<'static> = - match rustls_pemfile::private_key(&mut &key_pem[..]) { - Ok(Some(key)) => key, - _ => { - return Err(self.machine_st.open_permission_error( - self.machine_st.registers[2], - atom!("tls_server_negotiate"), - 3, - )); - } - }; + let cert_pem = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet")); + let key_pem = self.string_encoding_bytes(self.machine_st.registers[2], atom!("octet")); let stream0 = self.machine_st.get_stream_or_alias( self.machine_st.registers[3], @@ -7327,21 +7314,63 @@ impl Machine { 3, )?; - let config = match ServerConfig::builder() - .with_client_cert_verifier(Arc::new(GeminiClientVerifier)) - .with_single_cert(cert_chain, private_key) - { - Ok(config) => config, - Err(_) => { - return Err(self.machine_st.open_permission_error( - self.machine_st.registers[1], - atom!("tls_server_negotiate"), - 3, - )); + let cached_config = CONFIG_CACHE.with(|cache| { + cache.borrow().as_ref().and_then(|(cert, key, config)| { + (cert == &cert_pem && key == &key_pem).then(|| config.clone()) + }) + }); + + let config = match cached_config { + Some(config) => config, + None => { + let cert_chain: Vec> = + match rustls_pemfile::certs(&mut &cert_pem[..]).collect::, _>>() { + Ok(certs) if !certs.is_empty() => certs, + _ => { + return Err(self.machine_st.open_permission_error( + self.machine_st.registers[1], + atom!("tls_server_negotiate"), + 3, + )); + } + }; + + let private_key: PrivateKeyDer<'static> = + match rustls_pemfile::private_key(&mut &key_pem[..]) { + Ok(Some(key)) => key, + _ => { + return Err(self.machine_st.open_permission_error( + self.machine_st.registers[2], + atom!("tls_server_negotiate"), + 3, + )); + } + }; + + let config = match ServerConfig::builder() + .with_client_cert_verifier(Arc::new(GeminiClientVerifier)) + .with_single_cert(cert_chain, private_key) + { + Ok(config) => Arc::new(config), + Err(_) => { + return Err(self.machine_st.open_permission_error( + self.machine_st.registers[1], + atom!("tls_server_negotiate"), + 3, + )); + } + }; + + CONFIG_CACHE.with(|cache| { + *cache.borrow_mut() = + Some((cert_pem.clone(), key_pem.clone(), config.clone())); + }); + + config } }; - let mut conn = match ServerConnection::new(Arc::new(config)) { + let mut conn = match ServerConnection::new(config) { Ok(conn) => conn, Err(_) => { return Err(self.machine_st.open_permission_error(