diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index d521714848..5445c00444 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -6421,6 +6421,8 @@ Installer failure, mismatched version and failure to restore a previously active service share one rollback path. It stops the service, stages and compares all saved files before any replacement, and renames each into its recorded original path without following a new symlink or treating a directory as a destination. +Every restored destination must itself be regular and byte-equal to its saved +copy before recovery may activate the service or discard the backup. Files absent before the attempt remain absent after recovery, including the legacy/current binary alternative and `VERSION`. Metadata is preserved. A partial restore must not invoke the RETURN restart backstop. The backup remains diff --git a/scripts/installtests/pulse_auto_update_test.go b/scripts/installtests/pulse_auto_update_test.go index 667a5faf4a..bba9250e2f 100644 --- a/scripts/installtests/pulse_auto_update_test.go +++ b/scripts/installtests/pulse_auto_update_test.go @@ -647,6 +647,8 @@ func TestAutoUpdateBackupTransaction(t *testing.T) { {name: "rollback_copy_failure", fault: "restore_copy", outcome: "failure", retained: true}, {name: "corrupt_rollback_copy", fault: "restore_corrupt", outcome: "failure", retained: true}, {name: "rollback_rename_failure", fault: "restore_rename", outcome: "failure", retained: true}, + {name: "rollback_rename_noop", fault: "restore_noop", outcome: "failure", retained: true}, + {name: "rollback_destination_changed", fault: "restore_changed", outcome: "failure", retained: true}, {name: "version_restore_failure", fault: "version_rename", outcome: "failure", retained: true}, {name: "new_destination_directory", outcome: "directory", retained: true}, {name: "successful_update", success: true}, @@ -773,8 +775,10 @@ cp() { mv() { local dest="${*: -1}" if [[ "$FAULT" == restore_rename && "$dest" == "$INSTALL_DIR/bin/pulse" ]]; then return 1; fi + if [[ "$FAULT" == restore_noop && "$dest" == "$INSTALL_DIR/bin/pulse" ]]; then return 0; fi if [[ "$FAULT" == version_rename && "$dest" == "$INSTALL_DIR/VERSION" ]]; then return 1; fi - command mv "$@" + command mv "$@" || return + if [[ "$FAULT" == restore_changed && "$dest" == "$INSTALL_DIR/bin/pulse" ]]; then printf 'changed\n' > "$dest"; fi } curl() { local out="" url="" arg @@ -840,7 +844,7 @@ else [[ -z "$backup" || ! -e "$backup" ]] || { echo 'unneeded backup leaked'; exit 1; } fi # Incomplete file restoration must not activate an unverified executable. -case "$FAULT" in restore_tmp|restore_copy|restore_corrupt|restore_rename|version_rename|stop) +case "$FAULT" in restore_tmp|restore_copy|restore_corrupt|restore_rename|restore_noop|restore_changed|version_rename|stop) if [[ -f "$FIXTURE_DIR/service-operations" ]]; then ! grep -Eq '^(start|restart):' "$FIXTURE_DIR/service-operations" fi diff --git a/scripts/pulse-auto-update.sh b/scripts/pulse-auto-update.sh index d8ee4596ae..4b1a0a5995 100755 --- a/scripts/pulse-auto-update.sh +++ b/scripts/pulse-auto-update.sh @@ -552,7 +552,10 @@ perform_update() { done for i in "${!file_paths[@]}"; do if [[ "${file_present[i]}" == "true" ]]; then - if [[ -d "${file_paths[i]}" ]] || ! mv -f -- "${restore_files[i]}" "${file_paths[i]}"; then + if [[ -d "${file_paths[i]}" ]] || + ! mv -f -- "${restore_files[i]}" "${file_paths[i]}" || + [[ -L "${file_paths[i]}" ]] || + ! cmp -s -- "$backup_dir/${backup_names[i]}" "${file_paths[i]}"; then log error "Could not restore ${file_paths[i]}; backup retained at $backup_dir; manual recovery required" return 1 fi