Summary
LoggerSystem::Log correctly takes m_loggersMutex around the lookup/emplace into m_loggers. LoggerSystem::Shutdown does not take the mutex when it iterates and clears the same std::unordered_map. Concurrent calls to Log from any thread that is still alive during shutdown race against the iteration and clear.
Where
src/dll/Systems/LoggerSystem.cpp:22-35 — Shutdown iterates and calls m_loggers.clear() with no lock.
src/dll/Systems/LoggerSystem.hpp:40-76 — Log<T> takes std::scoped_lock _(m_loggersMutex); around the map access.
The reverse-order shutdown in App::Shutdown (src/dll/App.cpp:170-175) calls LoggerSystem::Shutdown last, so plugin worker threads and in-flight hook callbacks can still be running at that point — their DLLs are still mapped, and the RED4ext-installed detours have not been removed yet (those are detached later, in App::Destruct).
Why it matters
Data race on m_loggers:
Log may emplace into the map while Shutdown is mid-iteration → iterator invalidation, crash or torn read.
Log may read from the map while Shutdown has just called clear() → use-after-clear.
The window is narrow but not theoretical. The shutdown ordering bug tracked in #ISSUE-H6 makes this race more likely to fire in practice.
Suggested fix
Take the lock at the top of Shutdown and hold it for the whole flush + clear:
void LoggerSystem::Shutdown()
{
std::scoped_lock _(m_loggersMutex);
auto count = m_loggers.size();
spdlog::trace("Flusing {} logger(s)...", count);
for (auto& [plugin, logger] : m_loggers)
{
logger->flush();
}
m_loggers.clear();
spdlog::trace("{} logger(s) flushed", count);
}
Severity
High. The fix is one line; the cost of leaving it is a rare-but-real crash during shutdown that will be hell to debug from a user-submitted log.
Summary
LoggerSystem::Logcorrectly takesm_loggersMutexaround the lookup/emplace intom_loggers.LoggerSystem::Shutdowndoes not take the mutex when it iterates and clears the samestd::unordered_map. Concurrent calls toLogfrom any thread that is still alive during shutdown race against the iteration and clear.Where
src/dll/Systems/LoggerSystem.cpp:22-35—Shutdowniterates and callsm_loggers.clear()with no lock.src/dll/Systems/LoggerSystem.hpp:40-76—Log<T>takesstd::scoped_lock _(m_loggersMutex);around the map access.The reverse-order shutdown in
App::Shutdown(src/dll/App.cpp:170-175) callsLoggerSystem::Shutdownlast, so plugin worker threads and in-flight hook callbacks can still be running at that point — their DLLs are still mapped, and the RED4ext-installed detours have not been removed yet (those are detached later, inApp::Destruct).Why it matters
Data race on
m_loggers:Logmay emplace into the map whileShutdownis mid-iteration → iterator invalidation, crash or torn read.Logmay read from the map whileShutdownhas just calledclear()→ use-after-clear.The window is narrow but not theoretical. The shutdown ordering bug tracked in #ISSUE-H6 makes this race more likely to fire in practice.
Suggested fix
Take the lock at the top of
Shutdownand hold it for the whole flush + clear:Severity
High. The fix is one line; the cost of leaving it is a rare-but-real crash during shutdown that will be hell to debug from a user-submitted log.