From 7c544e37803580d746c461942c380f27366e2612 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sat, 5 Sep 2026 12:37:53 -0700 Subject: [PATCH] cap outstanding DNS-resolver threads Each DNS lookup for the SSRF guard spawns a thread that is never joined and can hang for the full OS resolver timeout, so a black-holed keyserver leaks one thread+stack per attempt. Track outstanding resolver threads with an atomic counter; over a cap of 4, report the host as unreachable instead of spawning another. Decrement only when the (possibly long-hung) lookup actually returns, so the cap reflects threads truly in flight. --- src/online.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/online.rs b/src/online.rs index 746de25..29e017b 100644 --- a/src/online.rs +++ b/src/online.rs @@ -153,8 +153,21 @@ fn resolve_and_guard(url: &str) -> Result, (GuardFail, String)> // resolver timeout and freeze the calling rip thread, so run it on a // spawned thread with a bounded deadline (mirrors autorip/libfreemkv). let addrs: Vec = { + use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::mpsc; const DNS_TIMEOUT: Duration = Duration::from_secs(10); + // Each resolver thread can hang for the OS timeout and is never joined, + // so a black-holed keyserver leaks one thread+stack per attempt. Cap the + // outstanding ones; over the cap, report the host unreachable instead. + const MAX_DNS_THREADS: usize = 4; + static DNS_THREADS: AtomicUsize = AtomicUsize::new(0); + if DNS_THREADS.fetch_add(1, Ordering::SeqCst) >= MAX_DNS_THREADS { + DNS_THREADS.fetch_sub(1, Ordering::SeqCst); + return Err(( + GuardFail::Unreachable, + "too many concurrent DNS resolutions in flight".into(), + )); + } let host = host.clone(); let (tx, rx) = mpsc::channel(); std::thread::spawn(move || { @@ -163,6 +176,9 @@ fn resolve_and_guard(url: &str) -> Result, (GuardFail, String)> .map(|it| it.collect::>()); // Receiver may be gone after the timeout — ignore the send error. let _ = tx.send(res); + // Decrement only when the (possibly long-hung) lookup actually + // returns, so the cap reflects threads truly in flight. + DNS_THREADS.fetch_sub(1, Ordering::SeqCst); }); match rx.recv_timeout(DNS_TIMEOUT) { Ok(Ok(addrs)) => addrs,