fix(bootstrap): check archive and free space before deleting chain data
The bootstrap removed blocks/, chainstate/ and notarizations/ first and only then opened the archive. An unsafe entry, a full disk or a corrupt member therefore left no chain, or a half-written one for dragonxd to start on. - Preflight before anything is deleted: every archive entry must be safe (no absolute path, no "..") and readable, and the disk must hold the extracted size plus max(256 MiB, 5%), counting what removing the old chain frees (a symlinked chain dir counts 0). Free space counts as unknown only when the probe errors, the capacity is 0 or the available size is -1; a known disk with 0 bytes free is a refusal. The old chain is kept on every preflight failure. - Protected members are skipped, compared case-insensitively: any path component starting with "wallet", any "database" component, a .cookie or *.conf file, and top-level *.dat files other than fee_estimates, peers and banlist. The number of non-critical member failures is logged. - The removal of the old chain is verified (a locked file fails the run instead of mixing old and new chain files). If the extraction fails or is aborted, the partial chain is removed again before the failure is published; isDone() turns true only after the worker's last filesystem change. - Cancel is disabled while the old chain data is being replaced, and quit aborts a running bootstrap. - A complete zip from an earlier attempt is reused on Retry; its checksum is re-verified. A download that cannot fit fails early, and a disk-full write error says so. Tests: the space rule (boundaries, unknown vs. full disk, saturation); planArchive sizes, zip-slip, garbage and truncated zips; the protected-name list; reclaimableBytes with symlinks; each preflight refusal keeps the old chain and wallet.dat byte-identical; a corrupt critical member removes the partial chain before the failure is published. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,16 @@ public:
|
||||
// Close the dialog (used by the UI sweep teardown).
|
||||
static void hide() { s_open = false; s_state = State::Confirm; }
|
||||
|
||||
// Quit: cancel a running Settings bootstrap and wait for its worker. main() ends in _Exit, so without this
|
||||
// a quit during extraction would kill the worker mid-file and leave a half-written chain; a cancelled
|
||||
// extraction removes the partial chain before the worker exits.
|
||||
static void abortForShutdown() {
|
||||
if (s_bootstrap) {
|
||||
s_bootstrap->cancel();
|
||||
s_bootstrap.reset();
|
||||
}
|
||||
}
|
||||
|
||||
static void render() {
|
||||
if (!s_app) return;
|
||||
if (!s_open) {
|
||||
@@ -211,9 +221,10 @@ private:
|
||||
ImGui::Spacing();
|
||||
|
||||
// Cancel button — once clicked, disable it so the request is visibly
|
||||
// acknowledged (and can't be re-fired) until the worker actually stops.
|
||||
// acknowledged (and can't be re-fired) until the worker actually stops. It is also disabled while
|
||||
// the old chain data is being replaced (cancelling then would only leave no chain at all).
|
||||
float btnW = 100.0f * dp;
|
||||
ImGui::BeginDisabled(s_cancelling);
|
||||
ImGui::BeginDisabled(s_cancelling || !prog.cancellable);
|
||||
if (TactileButton(TR("cancel"), ImVec2(btnW, 0))) {
|
||||
s_cancelling = true;
|
||||
s_bootstrap->cancel();
|
||||
|
||||
Reference in New Issue
Block a user