From e246699fef471773f78821622bef13eb5b3a3c9e Mon Sep 17 00:00:00 2001 From: reverse Date: Wed, 12 Aug 2026 12:46:52 +0000 Subject: [PATCH] =?UTF-8?q?oper:=20verify=20bcrypt=20passwords=20on=20a=20?= =?UTF-8?q?worker=20thread=20(Event::OperAuth),=20bounded=20=E2=80=94=20a?= =?UTF-8?q?=20bcrypt=20OPER=20no=20longer=20freezes=20the=20core,=20closin?= =?UTF-8?q?g=20the=20OPER-spam=20DoS;=20fast=20hashes=20stay=20inline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/coremods/core_oper.rs | 23 ++++++++++++++++++----- src/ircd.rs | 16 ++++++++++++++++ src/server.rs | 22 ++++++++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/coremods/core_oper.rs b/src/coremods/core_oper.rs index 195f848..6cdf0f3 100644 --- a/src/coremods/core_oper.rs +++ b/src/coremods/core_oper.rs @@ -121,13 +121,26 @@ impl Command for Oper { 2 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { - let (name, pass) = (¶ms[0], ¶ms[1]); - let level = s + let (name, pass) = (params[0].clone(), params[1].clone()); + let Some((hash, level)) = s .opers .iter() - .find(|(n, p, _)| n == name && crate::modules::password_hash::verify(p, pass)) - .map(|(_, _, lvl)| *lvl); - if let Some(level) = level { + .find(|(n, _, _)| *n == name) + .map(|(_, p, lvl)| (p.clone(), *lvl)) + else { + s.numeric(uid, ERR_PASSWDMISMATCH, ":Password incorrect"); + return CmdResult::Fail; + }; + // bcrypt is slow — verify it off the core thread (result arrives as OperAuth). + if hash.starts_with("$2") { + if !s.spawn_auth(uid, hash, pass, level) { + s.numeric(uid, ERR_PASSWDMISMATCH, ":Too many auth attempts, try again"); + return CmdResult::Fail; + } + return CmdResult::Ok; // pending; oper-up happens when the verify returns + } + // fast hashes (plaintext / sha* / pbkdf2) verify inline + if crate::modules::password_hash::verify(&hash, &pass) { s.oper_up(uid); crate::modules::operlevels::set(s, uid, level); // operlevels: KILL protection CmdResult::Ok diff --git a/src/ircd.rs b/src/ircd.rs index 39945a1..acb5bac 100644 --- a/src/ircd.rs +++ b/src/ircd.rs @@ -53,6 +53,13 @@ pub enum Event { uid: Uid, ident: Option, }, + /// A background OPER password verify finished. bcrypt is deliberately slow, so it + /// runs on a worker thread (see `Server::spawn_auth`) instead of freezing the core. + OperAuth { + uid: Uid, + ok: bool, + level: u32, + }, /// A module's async HTTP request finished. `tag` is `":"` /// so the core can route the reply back to the module that issued it (e.g. /// account registration, captcha verification). `status` is 0 on transport @@ -203,6 +210,15 @@ impl Ircd { crate::modules::ident::on_result(&mut self.server, uid, ident); self.try_register(uid); // ident may have been the last hold } + Event::OperAuth { uid, ok, level } => { + if ok { + self.server.oper_up(uid); + crate::modules::operlevels::set(&mut self.server, uid, level); + } else if self.server.users.contains_key(&uid) { + self.server + .numeric(uid, ERR_PASSWDMISMATCH, ":Password incorrect"); + } + } Event::HttpResult { uid, tag, diff --git a/src/server.rs b/src/server.rs index b4874ce..eca7157 100644 --- a/src/server.rs +++ b/src/server.rs @@ -450,6 +450,28 @@ impl Server { }); } + /// Verify an OPER password on a worker thread, delivering the result back as + /// `Event::OperAuth`. bcrypt is deliberately expensive (tens to hundreds of ms), + /// so running it inline would freeze the single-threaded core — and an OPER flood + /// against a bcrypt block would be a trivial DoS. Bounded so the flood can't spawn + /// unlimited hash threads; returns `false` when at capacity. + pub fn spawn_auth(&self, uid: Uid, hash: String, pass: String, level: u32) -> bool { + use std::sync::atomic::{AtomicUsize, Ordering}; + static ACTIVE: AtomicUsize = AtomicUsize::new(0); + const MAX_ACTIVE: usize = 16; + if ACTIVE.fetch_add(1, Ordering::Relaxed) >= MAX_ACTIVE { + ACTIVE.fetch_sub(1, Ordering::Relaxed); + return false; + } + let tx = self.event_tx.clone(); + std::thread::spawn(move || { + let ok = crate::modules::password_hash::verify(&hash, &pass); + ACTIVE.fetch_sub(1, Ordering::Relaxed); + let _ = tx.send(crate::ircd::Event::OperAuth { uid, ok, level }); + }); + true + } + /// A pre-registration `:server NOTICE * :*** ` line. pub(crate) fn notice_star(&self, uid: Uid, msg: &str) { self.send(uid, format!(":{} NOTICE * :*** {msg}", self.name));