From 1abad93646aca5640de968c52c5c1faa46bf986d Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 16 Feb 2026 21:52:12 -0700 Subject: [PATCH] fix: add TLS handshake timeout and fix accept loop deadlock Two issues in TlsListener::poll_accept: 1. No timeout on TLS handshakes: LazyConfigAcceptor waits indefinitely for ClientHello. Attackers that complete TCP handshake but never send TLS data create zombie futures in `in_progress` that never complete. Fix: wrap the entire handshake in tokio::time::timeout(15s). 2. Missing waker on new-connection pending path: when a TCP connection is accepted and the TLS handshake is pending, poll_accept returned Pending without calling wake_by_ref(). Since the TcpListener returned Ready (not Pending), no waker was registered for it. With edge- triggered epoll and no other wakeup source, the task sleeps forever and remaining connections in the kernel accept queue are never drained. Fix: add cx.waker().wake_by_ref() so the task immediately re-polls and continues draining the accept queue. --- TODO.md | 11 ++++++ core/src/net/tls.rs | 94 +++++++++++++++++++++++++++------------------ 2 files changed, 67 insertions(+), 38 deletions(-) diff --git a/TODO.md b/TODO.md index d7a06bb6d..6f6a6866f 100644 --- a/TODO.md +++ b/TODO.md @@ -171,6 +171,17 @@ Pending tasks for AI agents. Remove items when completed. | `sdk/base/lib/interfaces/Host.ts` | SDK `MultiHost.bindPort()` — no changes needed | | `core/src/db/model/public.rs` | Public DB model — port forward mapping | +- [ ] Switch `BackgroundJobRunner` from `Vec` to `FuturesUnordered` - @dr-bonez + + **Problem**: `BackgroundJobRunner` (in `core/src/util/actor/background.rs`) stores active jobs in a + `Vec` and polls ALL of them on every wakeup — O(n) per poll. This runs inside the same + `tokio::select!` as the WebServer accept loop (`core/src/net/web_server.rs:502`), so polling overhead + from active connections directly delays acceptance of new connections. + + **Fix**: Replace `jobs: Vec>` with `jobs: FuturesUnordered>`. + `FuturesUnordered` only polls woken futures — O(woken) per poll instead of O(n). The poll loop changes + from `iter_mut().filter_map(poll_unpin)` + `swap_remove` to `while poll_next_unpin().is_ready() {}`. + - [ ] Extract TS-exported types into a lightweight sub-crate for fast binding generation **Problem**: `make ts-bindings` compiles the entire `start-os` crate (with all dependencies: tokio, diff --git a/core/src/net/tls.rs b/core/src/net/tls.rs index 99f36e838..85897aec7 100644 --- a/core/src/net/tls.rs +++ b/core/src/net/tls.rs @@ -1,5 +1,6 @@ use std::sync::Arc; use std::task::{Poll, ready}; +use std::time::Duration; use futures::future::BoxFuture; use futures::stream::FuturesUnordered; @@ -170,14 +171,20 @@ where let (metadata, stream) = ready!(self.accept.poll_accept(cx)?); let mut tls_handler = self.tls_handler.clone(); let mut fut = async move { - let res = async { - let mut acceptor = - LazyConfigAcceptor::new(Acceptor::default(), BackTrackingIO::new(stream)); - let mut mid: tokio_rustls::StartHandshake> = - match (&mut acceptor).await { + let res = match tokio::time::timeout( + Duration::from_secs(15), + async { + let mut acceptor = LazyConfigAcceptor::new( + Acceptor::default(), + BackTrackingIO::new(stream), + ); + let mut mid: tokio_rustls::StartHandshake< + BackTrackingIO, + > = match (&mut acceptor).await { Ok(a) => a, Err(e) => { - let mut stream = acceptor.take_io().or_not_found("acceptor io")?; + let mut stream = + acceptor.take_io().or_not_found("acceptor io")?; let (_, buf) = stream.rewind(); if std::str::from_utf8(buf) .ok() @@ -201,46 +208,57 @@ where } } }; - let hello = mid.client_hello(); - if let Some(cfg) = tls_handler.get_config(&hello, &metadata).await { - let buffered = mid.io.stop_buffering(); - mid.io - .write_all(&buffered) - .await - .with_kind(ErrorKind::Network)?; - return Ok(match mid.into_stream(Arc::new(cfg)).await { - Ok(stream) => { - let s = stream.get_ref().1; - Some(( - TlsMetadata { - inner: metadata, - tls_info: TlsHandshakeInfo { - sni: s.server_name().map(InternedString::intern), - alpn: s - .alpn_protocol() - .map(|a| MaybeUtf8String(a.to_vec())), + let hello = mid.client_hello(); + if let Some(cfg) = tls_handler.get_config(&hello, &metadata).await { + let buffered = mid.io.stop_buffering(); + mid.io + .write_all(&buffered) + .await + .with_kind(ErrorKind::Network)?; + return Ok(match mid.into_stream(Arc::new(cfg)).await { + Ok(stream) => { + let s = stream.get_ref().1; + Some(( + TlsMetadata { + inner: metadata, + tls_info: TlsHandshakeInfo { + sni: s + .server_name() + .map(InternedString::intern), + alpn: s + .alpn_protocol() + .map(|a| MaybeUtf8String(a.to_vec())), + }, }, - }, - Box::pin(stream) as AcceptStream, - )) - } - Err(e) => { - tracing::trace!("Error completing TLS handshake: {e}"); - tracing::trace!("{e:?}"); - None - } - }); - } + Box::pin(stream) as AcceptStream, + )) + } + Err(e) => { + tracing::trace!("Error completing TLS handshake: {e}"); + tracing::trace!("{e:?}"); + None + } + }); + } - Ok(None) - } - .await; + Ok(None) + }, + ) + .await + { + Ok(res) => res, + Err(_) => { + tracing::trace!("TLS handshake timed out"); + Ok(None) + } + }; (tls_handler, res) } .boxed(); match fut.poll_unpin(cx) { Poll::Pending => { in_progress.push(fut); + cx.waker().wake_by_ref(); Poll::Pending } Poll::Ready((handler, res)) => {