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:
66
src/app.cpp
66
src/app.cpp
@@ -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().)
|
||||
|
||||
Reference in New Issue
Block a user