oper: verify bcrypt passwords on a worker thread (Event::OperAuth), bounded — a bcrypt OPER no longer freezes the core, closing the OPER-spam DoS; fast hashes stay inline

This commit is contained in:
Jean Chevronnet 2026-08-12 12:46:52 +00:00
parent 6795243d5f
commit e246699fef
3 changed files with 56 additions and 5 deletions

View file

@ -121,13 +121,26 @@ impl Command for Oper {
2 2
} }
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
let (name, pass) = (&params[0], &params[1]); let (name, pass) = (params[0].clone(), params[1].clone());
let level = s let Some((hash, level)) = s
.opers .opers
.iter() .iter()
.find(|(n, p, _)| n == name && crate::modules::password_hash::verify(p, pass)) .find(|(n, _, _)| *n == name)
.map(|(_, _, lvl)| *lvl); .map(|(_, p, lvl)| (p.clone(), *lvl))
if let Some(level) = level { 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); s.oper_up(uid);
crate::modules::operlevels::set(s, uid, level); // operlevels: KILL protection crate::modules::operlevels::set(s, uid, level); // operlevels: KILL protection
CmdResult::Ok CmdResult::Ok

View file

@ -53,6 +53,13 @@ pub enum Event {
uid: Uid, uid: Uid,
ident: Option<String>, ident: Option<String>,
}, },
/// 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 `"<module>:<detail>"` /// A module's async HTTP request finished. `tag` is `"<module>:<detail>"`
/// so the core can route the reply back to the module that issued it (e.g. /// 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 /// 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); crate::modules::ident::on_result(&mut self.server, uid, ident);
self.try_register(uid); // ident may have been the last hold 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 { Event::HttpResult {
uid, uid,
tag, tag,

View file

@ -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 * :*** <msg>` line. /// A pre-registration `:server NOTICE * :*** <msg>` line.
pub(crate) fn notice_star(&self, uid: Uid, msg: &str) { pub(crate) fn notice_star(&self, uid: Uid, msg: &str) {
self.send(uid, format!(":{} NOTICE * :*** {msg}", self.name)); self.send(uid, format!(":{} NOTICE * :*** {msg}", self.name));