From bbe4ee45673fcf1d80c6f25d701773827c190646 Mon Sep 17 00:00:00 2001 From: reverse Date: Wed, 19 Aug 2026 00:39:01 +0000 Subject: [PATCH] =?UTF-8?q?http:=20bound=20spawn=5Fhttp=20concurrency=20(h?= =?UTF-8?q?ttp=5Fmax=5Fconcurrent,=20default=2032)=20like=20spawn=5Fcrypto?= =?UTF-8?q?=20=E2=80=94=20it=20spawned=20one=20unbounded=20OS=20thread=20p?= =?UTF-8?q?er=20call,=20so=20a=20pre-auth=20VERIFY/REGISTER=20flood=20coul?= =?UTF-8?q?d=20exhaust=20threads=20and=20hammer=20the=20accounts=20backend?= =?UTF-8?q?;=20at=20capacity=20the=20command=20now=20fails=20with=20TEMPOR?= =?UTF-8?q?ARILY=5FUNAVAILABLE=20instead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/account_registration.rs | 24 ++++++++++++++++++++---- src/server.rs | 21 ++++++++++++++++++++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/modules/account_registration.rs b/src/modules/account_registration.rs index 1e3f920..075efa4 100644 --- a/src/modules/account_registration.rs +++ b/src/modules/account_registration.rs @@ -203,13 +203,21 @@ impl Command for Register { urlencode(&ip), port ); - s.spawn_http( + if !s.spawn_http( uid, format!("acctreg:register:{account}"), url, body, apikey_headers(s), - ); + ) { + s.fail( + uid, + "REGISTER", + "TEMPORARILY_UNAVAILABLE", + "The server is busy; please try again in a moment.", + ); + return CmdResult::Fail; + } CmdResult::Ok } } @@ -252,13 +260,21 @@ impl Command for Verify { urlencode(&account), urlencode(¶ms[1]) ); - s.spawn_http( + if !s.spawn_http( uid, format!("acctreg:verify:{account}"), url, body, apikey_headers(s), - ); + ) { + s.fail( + uid, + "VERIFY", + "TEMPORARILY_UNAVAILABLE", + "The server is busy; please try again in a moment.", + ); + return CmdResult::Fail; + } CmdResult::Ok } } diff --git a/src/server.rs b/src/server.rs index 708c514..158de95 100644 --- a/src/server.rs +++ b/src/server.rs @@ -462,6 +462,10 @@ impl Server { /// the core as `Event::HttpResult { uid, tag, .. }` — the same self-injection /// pattern as the DNS resolver, so a slow endpoint never blocks the main loop. /// `tag` is `":"`; the core routes the reply by its prefix. + /// Returns `false` when at capacity (`http_max_concurrent`, default 32) so the + /// caller can reject instead of spawning an unbounded number of threads — a + /// pre-auth flood (e.g. VERIFY) would otherwise exhaust threads and hammer the + /// backend. A `Drop` guard keeps the counter correct even if the task panics. pub fn spawn_http( &self, uid: Uid, @@ -469,10 +473,24 @@ impl Server { url: String, body: String, headers: Vec<(String, String)>, - ) { + ) -> bool { + use std::sync::atomic::{AtomicUsize, Ordering}; + static ACTIVE: AtomicUsize = AtomicUsize::new(0); + struct Guard; + impl Drop for Guard { + fn drop(&mut self) { + ACTIVE.fetch_sub(1, Ordering::Relaxed); + } + } + let max = self.conf_num("http_max_concurrent", 32usize); + if ACTIVE.fetch_add(1, Ordering::Relaxed) >= max { + ACTIVE.fetch_sub(1, Ordering::Relaxed); + return false; + } let tx = self.event_tx.clone(); let verify = self.conf_bool("http_tls_verify", true); std::thread::spawn(move || { + let _guard = Guard; // decrements even on panic let (status, body) = crate::http::post( &url, "application/x-www-form-urlencoded", @@ -489,6 +507,7 @@ impl Server { body, }); }); + true } /// Run an expensive credential operation (a KDF: bcrypt / pbkdf2) on a worker