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:
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user