/*************************************************************************
* ModernUO *
* Copyright 2019-2026 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: PasswordVerificationWorker.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
*************************************************************************/
using System;
using System.Collections.Concurrent;
using System.Threading;
using Server.Logging;
using Server.Misc;
using Server.Network;
namespace Server.Accounting.Security;
///
/// Work handed to the verification thread. Everything here is either an immutable string or a
/// reference the worker only carries -- the worker reads no game state and writes none.
///
internal sealed class PasswordVerificationJob
{
public Account Account;
public NetState State;
/// The stored hash at dispatch, used to verify and to guard the upgrade against a
/// password change that lands while this runs.
public string StoredHash;
/// Phrase to verify against .
public string VerifyPhrase;
/// Phrase to rehash from, or null when no upgrade is due.
public string RehashPhrase;
public PasswordProtectionAlgorithm TargetAlgorithm;
}
internal readonly struct PasswordVerificationOutcome
{
public readonly bool Verified;
/// The new hash, or null when the password did not verify or needed no upgrade.
public readonly string UpgradedPassword;
public PasswordVerificationOutcome(bool verified, string upgradedPassword)
{
Verified = verified;
UpgradedPassword = upgradedPassword;
}
}
///
/// Runs Argon2 off the game loop.
///
/// An Argon2 verify is ~8.9 ms, which is more than half a frame of frozen world for every login
/// attempt, successful or not. Measurement (docs/handoffs/2026-08-07-off-loop-argon2-hashing.md)
/// puts the on-loop saving at 3.5-8.9 ms per login: the hand-off costs ~220 ns, and the only real
/// residue is the loop's own work slowing while a memory-hard KDF evicts shared L3.
///
/// One worker, deliberately, for three reasons that agree:
/// - the per-login contention tax falls with concurrency but total loop damage rises, so one
/// hasher does the least harm to the loop;
/// - a single background hasher cannot cost the loop more than the inline verify under any
/// scheduling regime, because at worst it takes an equal share of one core -- which is what
/// makes the measurement extrapolate to hardware we cannot inspect. A pool breaks that bound;
/// - exactly one 16 MiB Argon2 arena is live at a time whatever the login volume, which answers
/// memory-exhaustion without a separate cap.
///
/// Throughput is ~110 verifies/sec. Wall-clock login latency is explicitly not a concern, so
/// head-of-line blocking during a rush costs nothing.
///
internal sealed class PasswordVerificationWorker
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(PasswordVerificationWorker));
///
/// Pending cap. Overflow rejects the login rather than verifying it inline: falling back to the
/// loop would let anyone who fills the queue steer the work back onto the thread this exists to
/// protect.
///
internal const int MaxPending = 128;
// Nothing signals the worker when a save freeze ends, so it re-checks on this interval -- but
// only while a save is in progress, never in steady state.
private const int SaveGatePollMs = 50;
private static PasswordVerificationWorker _instance;
///
/// Off-loop verification needs a spare core to move work to, which a 1-2 core host does not
/// have, and is pointless on a dev box or test shard where logins are rare and the simpler
/// path is easier to reason about.
///
internal static bool Enabled { get; } =
#if DEBUG
false;
#else
Environment.ProcessorCount >= 4;
#endif
private readonly Thread _thread;
private readonly AutoResetEvent _work = new(false);
private readonly ConcurrentQueue _queue = new();
// Its own Argon2, sharing no RNG with the loop's. Verification is static-backed and would be
// safe either way; hashing is not.
private readonly IPasswordProtection _argon2 = Argon2PasswordProtection.CreateIsolated();
private int _pending;
private volatile bool _exit;
private PasswordVerificationWorker()
{
_thread = new Thread(Execute)
{
IsBackground = true,
Name = "Password Verification"
};
_thread.Start();
}
// Created on first use, so a shard that never takes the off-loop path never allocates a thread.
private static PasswordVerificationWorker Instance => _instance ??= new PasswordVerificationWorker();
internal static int Pending => _instance?._pending ?? 0;
///
/// Queues a job. False when the queue is full, in which case the caller must reject the login
/// without verifying.
///
internal static bool TryEnqueue(PasswordVerificationJob job) => Instance.TryEnqueueCore(job);
private bool TryEnqueueCore(PasswordVerificationJob job)
{
if (Volatile.Read(ref _pending) >= MaxPending)
{
return false;
}
Interlocked.Increment(ref _pending);
_queue.Enqueue(job);
_work.Set();
return true;
}
///
/// Argon2 only, and only outside the save freeze. The freeze runs on the loop, so nothing new
/// can be queued while it holds; checking before each job bounds the overlap to whichever hash
/// was already in flight. PendingSave counts too -- the serialization threads are awake and
/// spinning on an empty queue by then, which is the worst moment to add a competitor.
///
private static bool CanRunNow() =>
World.WorldState is WorldState.Running or WorldState.WritingSave;
private void Execute()
{
while (!_exit)
{
if (_queue.IsEmpty)
{
// A kernel block at zero CPU. Set() during a hash leaves the event signalled, so a
// wake arriving mid-job is not lost.
_work.WaitOne();
continue;
}
if (!CanRunNow())
{
_work.WaitOne(SaveGatePollMs);
continue;
}
if (!_queue.TryDequeue(out var job))
{
continue;
}
Interlocked.Decrement(ref _pending);
PasswordVerificationOutcome outcome;
try
{
outcome = Compute(job);
}
catch (Exception ex)
{
// A verdict must still come back, or the connection waits forever for a reply.
logger.Error(ex, "Password verification failed for {Username}", job.Account?.Username);
outcome = new PasswordVerificationOutcome(false, null);
}
Core.LoopContext.Post(() => Apply(job, outcome));
}
}
private PasswordVerificationOutcome Compute(PasswordVerificationJob job)
{
if (!_argon2.ValidatePassword(job.StoredHash, job.VerifyPhrase))
{
return new PasswordVerificationOutcome(false, null);
}
return new PasswordVerificationOutcome(
true,
job.RehashPhrase == null ? null : _argon2.EncryptPassword(job.RehashPhrase)
);
}
private static void Apply(PasswordVerificationJob job, PasswordVerificationOutcome outcome)
{
var state = job.State;
// The connection may have gone while the hash ran. A dead NetState must not be revived, and
// nothing may be written on its behalf.
if (state?.Running != true)
{
return;
}
if (outcome.Verified && outcome.UpgradedPassword != null)
{
job.Account.ApplyPasswordUpgrade(job.StoredHash, outcome.UpgradedPassword, job.TargetAlgorithm);
}
AccountHandler.CompleteDeferredAccountLogin(state, job.Account, outcome.Verified);
}
/// Runs a job on the calling thread. The seam the tests drive, and the path taken when
/// off-loop verification is gated off.
internal static PasswordVerificationOutcome ComputeInline(PasswordVerificationJob job) =>
Instance.Compute(job);
internal static void Exit()
{
var instance = _instance;
if (instance == null)
{
return;
}
instance._exit = true;
instance._work.Set();
instance._thread.Join(TimeSpan.FromSeconds(5));
_instance = null;
}
}