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

@@ -63,6 +63,7 @@
#include <cstdio> // popen the rebuild helper
#include "util/perf_log.h"
#include "util/i18n.h"
#include "util/address_validation.h" // isValidRecipientAddress — payout validation at every start path (M-01)
#include "util/secure_vault.h"
#include <nlohmann/json.hpp>
@@ -2338,6 +2339,10 @@ void App::startMining(int threads)
return;
}
if (!state_.connected || !rpc_ || !worker_) return;
// Clamp the requested thread count to [1, logical cores] before setgenerate — an unclamped value
// (from a settings field or idle-scaling) would ask the daemon to spawn arbitrarily many threads. (M-08)
const int maxThreads = std::max(1, (int)std::thread::hardware_concurrency());
threads = std::clamp(threads, 1, maxThreads);
if (mining_toggle_in_progress_.exchange(true)) return; // already in progress
worker_->post([this, threads]() -> rpc::RPCWorker::MainCb {
@@ -2372,19 +2377,25 @@ void App::stopMining()
worker_->post([this]() -> rpc::RPCWorker::MainCb {
bool ok = false;
std::string errMsg;
try {
rpc::RPCClient::TraceScope trace("Mining tab / Stop mining");
rpc_->call("setgenerate", {false, 0});
ok = true;
} catch (const std::exception& e) {
DEBUG_LOGF("Failed to stop mining: %s\n", e.what());
errMsg = e.what();
DEBUG_LOGF("Failed to stop mining: %s\n", errMsg.c_str());
}
return [this, ok]() {
return [this, ok, errMsg]() {
mining_toggle_in_progress_.store(false);
if (ok) {
state_.mining.generate = false;
state_.mining.localHashrate = 0.0;
DEBUG_LOGF("Mining stopped\n");
} else {
// Don't silently leave generate=true as if it worked: tell the user and let the next
// getmininginfo refresh reconcile the true daemon state. (M-07)
ui::Notifications::instance().error("Failed to stop mining: " + errMsg);
}
};
});
@@ -2397,21 +2408,28 @@ void App::startPoolMining(int threads)
ui::Notifications::instance().warning("Pool mining is unavailable in this build");
return;
}
// Clamp to [1, logical cores] before the count reaches xmrig (M-06/M-08 pool path).
threads = std::clamp(threads, 1, std::max(1, (int)std::thread::hardware_concurrency()));
if (!xmrig_manager_)
xmrig_manager_ = std::make_unique<daemon::XmrigManager>();
// If already running, stop first (e.g. thread count change)
if (xmrig_manager_->isRunning()) {
xmrig_manager_->stop();
}
// Stop solo mining first if active
// Stop solo mining first if active (async via the RPC worker).
if (state_.mining.generate) stopMining();
// (the "stop the already-running miner first" step is done inside the control job below, in FIFO order)
daemon::XmrigManager::Config cfg;
cfg.pool_url = settings_->getPoolUrl();
cfg.worker_name = settings_->getPoolWorker();
// Validate the payout address at EVERY start entry point (manual Start button, idle auto-start, thread
// scaling) — not just the UI gate — since a stale/hand-edited/wrong-chain address here silently loses
// mining rewards. (M-01) worker_name IS the pool login the rewards are credited to (see below).
if (!cfg.worker_name.empty() && cfg.worker_name != "x" &&
!util::isValidRecipientAddress(cfg.worker_name)) {
ui::Notifications::instance().error(
"Pool payout address is not a valid DragonX address — mining not started.");
return;
}
// The algo follows the pool: official pools use their own algo (pool.dragonx.cc
// needs rx/dragonx, pool.dragonx.is rx/hush); custom hosts keep the setting.
cfg.algo = util::resolvePoolAlgo(cfg.pool_url, settings_->getPoolAlgo());
@@ -2443,34 +2461,46 @@ void App::startPoolMining(int threads)
return;
}
if (!xmrig_manager_->start(cfg)) {
std::string err = xmrig_manager_->getLastError();
DEBUG_LOGF("[ERROR] Pool mining: %s\n", err.c_str());
// Check for Windows Defender blocking (error 225 = ERROR_VIRUS_INFECTED)
if (err.find("error 225") != std::string::npos ||
err.find("virus") != std::string::npos) {
ui::Notifications::instance().error(
"Windows Defender blocked xmrig. Add exclusion for %APPDATA%\\ObsidianDragon");
// Run the blocking stop(if running)+start on the serialized mining-control thread so the render thread
// never blocks on stop()'s SIGTERM->SIGKILL->join; marshal the spawn result back to the UI. (M-03/L-06/
// L-08/L-09/L-13). cfg was fully built above on this (main) thread.
daemon::XmrigManager::Config cfgCopy = cfg;
postMiningControl([this, cfgCopy]() {
if (xmrig_manager_->isRunning()) xmrig_manager_->stop(3000);
const bool ok = xmrig_manager_->start(cfgCopy);
const std::string err = ok ? std::string() : xmrig_manager_->getLastError();
if (!worker_) return;
worker_->post([this, ok, err]() -> rpc::RPCWorker::MainCb {
return [this, ok, err]() {
if (ok) {
// Miner spawned — it still needs a few seconds to connect to the pool and start hashing.
pool_starting_.store(true, std::memory_order_relaxed);
ui::Notifications::instance().info("Starting pool miner — connecting to the pool…");
} else {
DEBUG_LOGF("[ERROR] Pool mining: %s\n", err.c_str());
// Windows Defender blocking (error 225 = ERROR_VIRUS_INFECTED)
if (err.find("error 225") != std::string::npos || err.find("virus") != std::string::npos) {
ui::Notifications::instance().error(
"Windows Defender blocked xmrig. Add exclusion for %APPDATA%\\ObsidianDragon");
#ifdef _WIN32
// Offer to open Windows Security settings
pending_antivirus_dialog_ = true;
pending_antivirus_dialog_ = true;
#endif
} else {
ui::Notifications::instance().error("Failed to start pool miner: " + err);
}
} else {
// Miner spawned — it still needs a few seconds to connect to the pool and start hashing.
pool_starting_.store(true, std::memory_order_relaxed);
ui::Notifications::instance().info("Starting pool miner — connecting to the pool…");
}
} else {
ui::Notifications::instance().error("Failed to start pool miner: " + err);
}
}
};
});
});
}
void App::stopPoolMining()
{
if (xmrig_manager_ && xmrig_manager_->isRunning()) {
xmrig_manager_->stop(3000);
}
if (!xmrig_manager_) return;
// Off the render thread — stop()'s SIGTERM->SIGKILL->join can block up to ~3s. (M-03/L-06/L-08/L-09)
postMiningControl([this]() {
if (xmrig_manager_->isRunning()) xmrig_manager_->stop(3000);
});
}
// ============================================================================