fix(i18n): thread-safe translation for the daemon monitor thread (don't race loadLanguage)

Concurrency audit: I18n::translate() returns a const char* pointing INTO strings_, and loadLanguage()
clears/rebuilds strings_ on the main thread at a language switch. The EmbeddedDaemon state/monitor callback
builds its status strings via TR() off the main thread, so a concurrent language switch could free the
map node out from under that pointer — a use-after-free data race.

- Add a shared_mutex guarding strings_: loadLanguage() takes it exclusively for the whole clear+rebuild
  (its helpers loadBuiltinEnglish/overlayTranslations are called only from there and don't lock, so no
  recursion); a new translateCopy()/trCopy() takes a shared lock and returns an owned std::string.
- The lock-free translate()/TR() stays for main-thread UI (same thread as the writer, so never races).
- The four daemon-state setDaemonStatus(TR(...)) calls in the monitor callback now use util::trCopy(...).

Synchronization only; no fund-path call touched. Threading fix — clean build + ctest; the race is runtime-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 18:38:46 -05:00
co-authored by Claude Opus 4.8
parent 6301cd5984
commit 5563becad2
3 changed files with 40 additions and 5 deletions
+22 -1
View File
@@ -4,6 +4,8 @@
#pragma once
#include <mutex>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <vector>
@@ -38,7 +40,16 @@ public:
* @return Translated string, or key if not found
*/
const char* translate(const char* key) const;
/**
* @brief Thread-safe translate: returns a COPY under a read lock.
*
* translate() hands back a pointer into the string table, which loadLanguage() rewrites on the
* main thread. Any caller off the main thread (e.g. the EmbeddedDaemon monitor thread) MUST use
* this instead; main-thread UI code keeps using translate()/TR().
*/
std::string translateCopy(const char* key) const;
/**
* @brief Get current locale
*/
@@ -66,6 +77,9 @@ private:
void overlayTranslations(const nlohmann::json& translations);
std::string current_locale_ = "en";
// Guards strings_/current_locale_ against loadLanguage() (main thread, writer) racing
// translateCopy() (off-thread readers). The lock-free translate() is main-thread only.
mutable std::shared_mutex strings_mutex_;
std::unordered_map<std::string, std::string> strings_;
std::vector<std::pair<std::string, std::string>> available_languages_;
};
@@ -79,6 +93,13 @@ inline const char* tr(const char* key) {
return I18n::instance().translate(key);
}
/**
* @brief Thread-safe translation returning an owned copy. Use from any non-main thread.
*/
inline std::string trCopy(const char* key) {
return I18n::instance().translateCopy(key);
}
/**
* @brief Map a raw daemon / JSON-RPC error string to a short, friendly, TRANSLATED explanation.
*