fix(mining): remediate mining-tab audit (22 findings) — crash-safety, async control, validation, math

Fixes all 22 confirmed findings from the mining-tab audit (10 Medium, 12 Low; 0 Critical/High),
adversarially reviewed (6 follow-ups found + fixed, incl. the review-caught idle-auto-start bypass
and a wrong benchmark-restore condition).

Crash-safety & lifecycle:
- M-04: join a stale/finished monitor thread in XmrigManager::start() and ~XmrigManager so an xmrig
  crash-then-restart (or quit) no longer std::terminate()s the wallet.
- L-03/L-10: surface an unexpected miner exit once and clear the stale running flag.

UI never blocks (M-03/L-06/L-08/L-09/L-13): pool start/stop now run on a dedicated serialized FIFO
mining-control thread (joined before teardown), so the ~13 call sites don't block the render thread on
stop()'s SIGTERM->SIGKILL->join; the spawn result marshals back to the UI.

Miner-process / pool trust boundary:
- M-01: validate the payout address (util::isValidRecipientAddress) at EVERY start path — the UI gate
  AND App::startPoolMining() (idle auto-start / thread scaling) — so a stale/wrong-chain address can't
  silently lose rewards.
- M-09: SSRF guard skips the background pool-stats GET for loopback/private/link-local/single-label hosts.
- M-02/L-02: cap the pool-stats + xmrig-API HTTP response bodies.
- L-01: write the xmrig config 0600 at creation (POSIX open with mode) — no world/group-readable window.
- M-10: reject shell-metacharacter binary paths before the version popen (excluding '()' so Program Files
  (x86) still works).

Solo mining: M-06/M-08 clamp thread count to [1, cores] at the setgenerate/xmrig boundary; M-07 notify +
don't lie on stop failure.

Correctness: L-05 block-time constant 75->150s (chainparams); M-05 discloses pool-mode "Est. Daily" as a
rough solo-equivalent; L-04/L-11/L-12 benchmark lifecycle (cancel on nav-away / mode-switch with restore,
skip rebalance mid-benchmark); L-07 honor cancel mid-extract in both the xmrig and daemon updaters.

Two new i18n keys back-filled across all 8 languages; CJK subset font rebuilt for the new glyphs.
Verified across full-node, lite, and Windows builds; tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-18 14:53:46 -05:00
parent 6ee81a5abe
commit 06afbee4f8
24 changed files with 285 additions and 47 deletions

View File

@@ -147,6 +147,45 @@ void App::wipeSecrets()
sodium_memzero(import_key_input_, sizeof(import_key_input_)); // pasted private key (SECRET)
}
// Enqueue a blocking xmrig start/stop op onto the dedicated serialized control thread so the render thread
// never blocks on stop()'s SIGTERM->SIGKILL->join, while start/stop still execute in FIFO order. (M-03/…)
void App::postMiningControl(std::function<void()> job)
{
{
std::lock_guard<std::mutex> lk(mining_ctl_mutex_);
if (mining_ctl_stop_) return; // shutting down — don't enqueue new mining ops
if (!mining_ctl_thread_.joinable()) {
mining_ctl_thread_ = std::thread([this]() {
for (;;) {
std::function<void()> j;
{
std::unique_lock<std::mutex> lk(mining_ctl_mutex_);
mining_ctl_cv_.wait(lk, [this]{ return mining_ctl_stop_ || !mining_ctl_queue_.empty(); });
if (mining_ctl_stop_) return; // abandon any pending jobs on shutdown
j = std::move(mining_ctl_queue_.front());
mining_ctl_queue_.pop_front();
}
j();
}
});
}
mining_ctl_queue_.push_back(std::move(job));
}
mining_ctl_cv_.notify_one();
}
// Signal the mining-control thread to stop and join it. Called at shutdown BEFORE xmrig_manager_ is stopped
// or destroyed, so no control job runs concurrently with teardown. Idempotent.
void App::stopMiningControlThread()
{
{
std::lock_guard<std::mutex> lk(mining_ctl_mutex_);
mining_ctl_stop_ = true;
}
mining_ctl_cv_.notify_all();
if (mining_ctl_thread_.joinable()) mining_ctl_thread_.join();
}
namespace {
// How often auto-balance re-evaluates the pool while active. Long, because switching
// restarts the miner (drops in-flight shares + reconnect); the incumbent stickiness
@@ -166,6 +205,7 @@ void App::updatePoolAutoBalance()
if (!supportsPoolMining()) return; // pool mining is available in both builds (solo is full-node only)
if (settings_->getPoolSelectMode() != config::Settings::PoolSelectMode::AutoBalance) return;
if (!settings_->getPoolMode()) return; // only while POOL mode is selected
if (ui::IsMiningBenchmarkActive()) return; // don't auto-switch pools mid-benchmark — it restarts xmrig at the wrong thread count (L-12)
const long long now = steadyNowMs();
const bool intervalDue = (last_balance_eval_ms_ == 0) ||
@@ -965,6 +1005,20 @@ void App::update()
}
}
// Surface an unexpected miner exit (crash / OOM-kill / external SIGKILL) once, and clear the stale
// running flag so the UI and auto-balance don't keep believing it's still hashing. (L-03, L-10)
if (xmrig_manager_ && state_.pool_mining.xmrig_running
&& xmrig_manager_->getState() == daemon::XmrigManager::State::Error) {
state_.pool_mining.xmrig_running = false;
state_.pool_mining.hashrate_10s = 0.0;
state_.pool_mining.hashrate_60s = 0.0;
state_.pool_mining.hashrate_15m = 0.0;
pool_starting_.store(false, std::memory_order_relaxed);
const std::string err = xmrig_manager_->getLastError();
ui::Notifications::instance().error(err.empty() ? "Miner stopped unexpectedly."
: ("Miner stopped: " + err));
}
// Poll xmrig stats every ~2 seconds (use a simple toggle)
static bool xmrig_poll_tick = false;
xmrig_poll_tick = !xmrig_poll_tick;
@@ -1797,6 +1851,10 @@ void App::render()
if ((current_page_ == ui::NavPage::Console || current_page_ == ui::NavPage::LiteConsole)
&& settings_ && settings_->getConsoleAutoFocus())
console_tab_.requestInputFocus();
// Leaving the Mining tab → cancel a running thread benchmark so the miner isn't abandoned at a
// benchmark step. (L-04)
if (prev_page_ == ui::NavPage::Mining && current_page_ != ui::NavPage::Mining)
ui::CancelMiningBenchmark(this);
prev_page_ = current_page_;
}
if (page_alpha_ < 1.0f) {
@@ -5595,6 +5653,10 @@ void App::beginShutdown()
fast_worker_->requestStop();
}
// Drain + join the mining-control thread FIRST so no async start/stop job runs while we tear the miner
// down here (avoids two threads driving xmrig_manager_ during shutdown). (M-03 cluster)
stopMiningControlThread();
// Stop xmrig pool miner before stopping the daemon
if (xmrig_manager_ && xmrig_manager_->isRunning()) {
shutdown_status_ = "Stopping pool miner...";
@@ -6380,6 +6442,10 @@ void App::renderLoadingOverlay(float contentH)
void App::shutdown()
{
// Ensure the mining-control thread is stopped + joined (idempotent; beginShutdown already did it on the
// normal quit path, but shutdown() can also run without it). Must precede xmrig_manager_ teardown.
stopMiningControlThread();
// Wipe any copied secret from the OS clipboard before we exit — the 45s auto-clear timer
// never fires if the user quits sooner, which would otherwise leave a key/seed resident.
// (ImGui context is still alive here; App::shutdown() runs before ImGui::DestroyContext().)