fix(mining): harden xmrig updater per adversarial review
Addresses confirmed findings from the multi-lens review of the updater: - Cancelable + live progress (was: download uncancelable, progress stuck at 0%, closing the dialog mid-download blocked the UI thread on the worker join). Wire a libcurl CURLOPT_XFERINFOFUNCTION that publishes byte counts and returns abort when cancel() is requested; add a Cancel button. The dialog's destructor now aborts the transfer promptly, so closing mid-download no longer freezes the UI. - Graceful "unavailable" instead of a red error on platforms with no published build (macOS / ARM): new terminal State::Unavailable rendered neutrally, not as a failure. - Install-time running guard (TOCTOU): App::isPoolMinerRunning() re-checked in the dialog before each install, so a dialog opened before mining started can't replace a live binary. - Size caps: CURLOPT_MAXFILESIZE on the download and a per-archive-member ceiling before decomphressing into memory, to bound an attacker-controlled archive. - Distinguish a local read failure of the downloaded archive from a checksum mismatch (was reported misleadingly as "possible tampering"). - Reword the dialog's verification note to "checked against the release's published SHA-256 checksum" (integrity, not authenticity — see the signing note below). Not fixed here (needs your input): WinRing0x64.sys has no per-file hash published, but it is covered by the verified archive checksum (it is inside the verified zip); and the release is not cryptographically signed — checksums and binary share one trust root. Adding a pinned-key ed25519/minisign signature is the real supply-chain hardening and needs an offline signing key + a release-process change. Both variants build; suite passes; live worker re-verified end-to-end on linux-x64. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,7 @@ public:
|
||||
case St::Checking: renderChecking(dp, p); break;
|
||||
case St::UpToDate:
|
||||
case St::UpdateAvailable: renderPrompt(dp, p); break;
|
||||
case St::Unavailable: renderUnavailable(dp, p); break;
|
||||
case St::Downloading:
|
||||
case St::Verifying:
|
||||
case St::Extracting: renderProgress(dp, p); break;
|
||||
@@ -70,11 +71,35 @@ private:
|
||||
|
||||
static float fullW() { return ImGui::GetContentRegionAvail().x; }
|
||||
|
||||
// Install button that refuses (with a note) while the miner is running, re-checked live so a
|
||||
// dialog opened before mining started can't replace a now-running binary (TOCTOU guard).
|
||||
static void installAction(const char* label) {
|
||||
using namespace material;
|
||||
if (s_app->isPoolMinerRunning()) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
|
||||
"Stop mining before updating the miner.");
|
||||
return;
|
||||
}
|
||||
if (TactileButton(label, ImVec2(fullW(), 0)))
|
||||
s_updater->startInstall(resources::getDaemonDirectory());
|
||||
}
|
||||
|
||||
static void renderChecking(float, const Progress&) {
|
||||
using namespace material;
|
||||
Type().text(TypeStyle::Body2, "Checking for the latest miner…");
|
||||
}
|
||||
|
||||
static void renderUnavailable(float, const Progress& p) {
|
||||
using namespace material;
|
||||
Type().text(TypeStyle::Subtitle2, "Miner updates unavailable");
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", p.status_text.empty()
|
||||
? "No miner build is available for this platform."
|
||||
: p.status_text.c_str());
|
||||
ImGui::Spacing();
|
||||
if (TactileButton("Close", ImVec2(fullW(), 0))) s_open = false;
|
||||
}
|
||||
|
||||
static void renderPrompt(float, const Progress& p) {
|
||||
using namespace material;
|
||||
const std::string installed = p.installed_tag.empty() ? "none" : p.installed_tag;
|
||||
@@ -85,17 +110,15 @@ private:
|
||||
ImGui::Text("Installed: %s", installed.c_str());
|
||||
ImGui::Spacing();
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
|
||||
"The download is verified against its published SHA-256 checksum before install.");
|
||||
"The download is checked against the release's published SHA-256 checksum before install.");
|
||||
ImGui::Spacing();
|
||||
if (TactileButton("Download & install", ImVec2(fullW(), 0)))
|
||||
s_updater->startInstall(resources::getDaemonDirectory());
|
||||
installAction("Download & install");
|
||||
} else {
|
||||
Type().text(TypeStyle::Subtitle2, "The miner is up to date");
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("Installed: %s", p.latest_tag.c_str());
|
||||
ImGui::Spacing();
|
||||
if (TactileButton("Reinstall", ImVec2(fullW(), 0)))
|
||||
s_updater->startInstall(resources::getDaemonDirectory());
|
||||
installAction("Reinstall");
|
||||
}
|
||||
ImGui::Spacing();
|
||||
if (TactileButton("Close", ImVec2(fullW(), 0))) s_open = false;
|
||||
@@ -120,6 +143,12 @@ private:
|
||||
ImGui::Dummy(ImVec2(0, barH));
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("%s", p.status_text.c_str());
|
||||
ImGui::Spacing();
|
||||
// Cancel aborts the in-flight transfer promptly (the curl progress callback returns abort).
|
||||
if (TactileButton("Cancel", ImVec2(fullW(), 0))) {
|
||||
s_updater->cancel();
|
||||
s_open = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void renderDone(float, const Progress& p) {
|
||||
@@ -144,8 +173,7 @@ private:
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", p.error.empty() ? "Unknown error." : p.error.c_str());
|
||||
ImGui::Spacing();
|
||||
if (TactileButton("Retry", ImVec2(fullW(), 0)))
|
||||
s_updater->startInstall(resources::getDaemonDirectory());
|
||||
installAction("Retry");
|
||||
ImGui::Spacing();
|
||||
if (TactileButton("Close", ImVec2(fullW(), 0))) s_open = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user