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:
2026-06-06 18:35:17 -05:00
parent 5c87bc6e87
commit 98e0cce8ec
4 changed files with 93 additions and 11 deletions

View File

@@ -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;
}