Skip to content

Commit d544dbe

Browse files
VijitSingh97claude
andauthored
fix(#549,#551): verify plaintext archives before restore; clean up + restart on backup failure (#571)
restore (#549): the plaintext branch extracted with no integrity check — mirror the encrypted branch's full-stream 'tar -tzf' verify so a truncated/corrupt archive is refused before anything is written. backup (#551): guard the plaintext 'sudo tar' (errexit fired straight through before), remove the partial root-owned archive on failure, and restart the stack when it was running — both branches — before erroring with the cause. Tier-1 tests: truncated plaintext restore is rejected with config.json/.env byte-identical; a shadowed-tar backup failure removes the partial archive, restarts the running stack (compose up in the docker log), and exits non-zero. Closes #549 Closes #551 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent e9aa04f commit d544dbe

3 files changed

Lines changed: 81 additions & 4 deletions

File tree

docs/operations.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -341,8 +341,10 @@ is typo'd away must fail loudly, not archive your onion keys in plaintext while
341341
To write a plaintext archive on purpose, pass `--no-encrypt` (an empty passphrase at the interactive
342342
prompt does the same, with a warning).
343343

344-
If the stack is running, `backup` stops it for a consistent copy and restarts it when done. Pass
345-
`-y` / `--yes` to skip both prompts (low-space warning, stop-the-stack question).
344+
If the stack is running, `backup` stops it for a consistent copy and restarts it when done. A
345+
failed backup (disk full mid-archive, for example) removes the partial archive and still restarts
346+
the stack before reporting the error. Pass `-y` / `--yes` to skip both prompts (low-space warning,
347+
stop-the-stack question).
346348

347349
Include the blockchains (larger, slower) with:
348350

@@ -360,7 +362,8 @@ To recover (on a new machine, or after a wipe) copy the archive back and run:
360362

361363
`restore` detects the format from the archive itself — encrypted backups ask for the passphrase
362364
(or read `PITHEAD_BACKUP_PASSPHRASE`), and plaintext archives from earlier releases restore
363-
unchanged, no flag needed. A wrong passphrase fails before anything on disk is touched. `restore`
365+
unchanged, no flag needed. A wrong passphrase, or a corrupt or truncated archive of either format,
366+
fails before anything on disk is touched. `restore`
364367
prompts before overwriting anything (pass `-y` / `--yes` to skip). It puts the files back, fixes
365368
Tor key ownership so the onion address returns unchanged, and restores hashrate history and
366369
dashboard settings.

pithead

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1229,10 +1229,17 @@ stack_backup() {
12291229
openssl enc -aes-256-cbc -pbkdf2 -iter 600000 -salt \
12301230
-pass fd:3 -out "$archive" 3< <(printf '%s' "$pass")); then
12311231
rm -f "$archive"
1232+
[ "$was_running" -eq 1 ] && stack_up
12321233
error "Backup failed — the partial archive was removed."
12331234
fi
12341235
else
1235-
(umask 077 && sudo tar -czf "$archive" -C "/" "${rel[@]}")
1236+
# sudo tar itself opens $archive, so a failed run can still leave a root-owned partial
1237+
# file behind — guard it the same way as the encrypted branch above (#551).
1238+
if ! (umask 077 && sudo tar -czf "$archive" -C "/" "${rel[@]}"); then
1239+
sudo rm -f "$archive"
1240+
[ "$was_running" -eq 1 ] && stack_up
1241+
error "Backup failed — the partial archive was removed."
1242+
fi
12361243
fi
12371244
sudo chown "$REAL_USER":"$REAL_USER" "$archive"
12381245
chmod 600 "$archive"
@@ -1319,6 +1326,12 @@ stack_restore() {
13191326
-pass fd:3 -in "$archive" 2>/dev/null 3< <(printf '%s' "$pass") |
13201327
tar -tzf - >/dev/null 2>&1 ||
13211328
error "Archive fails integrity verification (tampered or truncated) — nothing was restored."
1329+
else
1330+
# Same full-stream verify as the encrypted branch above, minus the decrypt step: a
1331+
# truncated/corrupt plaintext archive would otherwise abort tar MID-EXTRACTION, leaving
1332+
# the live config half-overwritten (#549).
1333+
tar -tzf "$archive" >/dev/null 2>&1 ||
1334+
error "Archive fails integrity verification (tampered or truncated) — nothing was restored."
13221335
fi
13231336

13241337
log "Restoring from $archive ..."

tests/stack/run.sh

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3513,6 +3513,22 @@ assert_rc "plaintext archive still restores" "$rc" "0"
35133513
assert_eq "plaintext restore brings back the Caddyfile" "$(cat "$BK/Caddyfile")" "CADDY-ORIG"
35143514
rm -f "$BK"/backups/pithead-backup-*
35153515

3516+
# 6b) Truncated plaintext archive (#549): mirrors the encrypted-branch tamper/truncation check
3517+
# (4b above) on the gzip path — a truncated archive must be rejected by a full-stream `tar -tzf`
3518+
# verify BEFORE extraction, with nothing written, instead of half-overwriting config.json/.env.
3519+
out="$(cd "$BK" && PATH="$BK/bin:$PATH" ./pithead backup -y --no-encrypt 2>&1)"
3520+
plain_trunc_src="$(ls "$BK"/backups/pithead-backup-*.tar.gz 2>/dev/null | head -1)"
3521+
config_before="$(cat "$BK/config.json")"
3522+
env_before="$(cat "$BK/.env")"
3523+
head -c "$(($(wc -c <"$plain_trunc_src") / 2))" "$plain_trunc_src" >"$BK/backups/truncated-plain.tar.gz"
3524+
out="$(cd "$BK" && PATH="$BK/bin:$PATH" ./pithead restore -y "$BK/backups/truncated-plain.tar.gz" 2>&1)"
3525+
rc=$?
3526+
[ "$rc" -ne 0 ] && ok "truncated plaintext archive exits non-zero" || bad "truncated plaintext archive exits non-zero" "rc=0"
3527+
assert_contains "truncated plaintext archive names integrity failure" "$out" "integrity"
3528+
assert_eq "truncated plaintext archive leaves config.json untouched" "$(cat "$BK/config.json")" "$config_before"
3529+
assert_eq "truncated plaintext archive leaves .env untouched" "$(cat "$BK/.env")" "$env_before"
3530+
rm -f "$BK"/backups/pithead-backup-* "$BK/backups/truncated-plain.tar.gz"
3531+
35163532
# 7) A failed encrypted backup (openssl dies mid-stream) removes the partial archive.
35173533
cat >"$BK/bin/openssl" <<'EOF'
35183534
#!/usr/bin/env bash
@@ -3542,6 +3558,51 @@ rc=$?
35423558
assert_contains "encrypted restore w/o passphrase explains" "$out" "PITHEAD_BACKUP_PASSPHRASE"
35433559
rm -f "$BK"/backups/pithead-backup-*
35443560

3561+
echo "== black-box: failed plaintext backup restarts a running stack, removes the partial archive (#551) =="
3562+
# Companion to the #549 test above: a failed tar must not strand the stack stopped, nor leave a
3563+
# partial (root-owned) archive that looks like a valid backup. Shadow tar to fail unconditionally
3564+
# and simulate a RUNNING stack (was_running=1), so the failure path must call stack_up for real —
3565+
# proven here by "compose up" showing up in the docker log, not by stubbing stack_up away.
3566+
FB="$SANDBOX/failbackup"
3567+
mkdir -p "$FB/build/tari" "$FB/data/tor" "$FB/data/dashboard" "$FB/bin"
3568+
cp "$STACK" "$FB/pithead"
3569+
cp "$ROOT/build/tari/config.toml.template" "$FB/build/tari/"
3570+
cat >"$FB/bin/docker" <<'EOF'
3571+
#!/usr/bin/env bash
3572+
echo "[docker] $*" >>"${DOCKER_LOG:-/dev/null}"
3573+
case "$*" in
3574+
"compose ps --status running -q") echo cid123 ;; # non-empty -> stack treated as RUNNING
3575+
esac
3576+
exit 0
3577+
EOF
3578+
cat >"$FB/bin/sudo" <<'EOF'
3579+
#!/usr/bin/env bash
3580+
[ "$1" = "chown" ] && exit 0
3581+
exec "$@"
3582+
EOF
3583+
cat >"$FB/bin/tar" <<'EOF'
3584+
#!/usr/bin/env bash
3585+
exit 1
3586+
EOF
3587+
chmod +x "$FB/bin/docker" "$FB/bin/sudo" "$FB/bin/tar"
3588+
cat >"$FB/.env" <<EOF
3589+
MONERO_ONION_ADDRESS=mona.onion
3590+
TARI_ONION_ADDRESS=taria.onion
3591+
P2POOL_ONION_ADDRESS=p2pa.onion
3592+
PROXY_AUTH_TOKEN=FBTOKEN
3593+
HOST_IP=box.lan
3594+
DEPLOYMENT_COMPLETED=true
3595+
COMPOSE_PROFILES=local_node
3596+
EOF
3597+
printf '{ "monero": {"mode":"local","wallet_address":"%s","node_username":"u","node_password":"p"}, "tari":{"wallet_address":"T"}, "p2pool":{"pool":"main"}, "dashboard":{"secure":true,"host":"box.lan"} }\n' "$WALLET" >"$FB/config.json"
3598+
3599+
out="$(cd "$FB" && DOCKER_LOG="$FB/docker.log" PATH="$FB/bin:$PATH" ./pithead backup -y --no-encrypt 2>&1)"
3600+
rc=$?
3601+
[ "$rc" -ne 0 ] && ok "failed plaintext backup (running stack) exits non-zero" || bad "failed plaintext backup (running stack) exits non-zero" "rc=0"
3602+
assert_contains "failed plaintext backup names the cause" "$out" "partial archive was removed"
3603+
assert_eq "failed plaintext backup leaves no archive behind" "$(ls "$FB"/backups/pithead-backup-* 2>/dev/null | head -1)" ""
3604+
assert_contains "failed plaintext backup restarts the stack" "$(cat "$FB/docker.log" 2>/dev/null)" "compose up"
3605+
35453606
echo "== black-box: reset-dashboard targets .env dirs, not config.json (#139) =="
35463607
# reset-dashboard must wipe the LIVE deployment's data dirs (from .env), not a path the user may
35473608
# have edited into config.json without applying. docker = noop; sudo only LOGS (never executes the

0 commit comments

Comments
 (0)