diff --git a/host-setup/bootstrap.sh b/host-setup/bootstrap.sh index 84526cda..9c18cdcc 100755 --- a/host-setup/bootstrap.sh +++ b/host-setup/bootstrap.sh @@ -92,6 +92,7 @@ resolve_ref() { # An unauthenticated request is rate limited per address, so a busy network can lose the lookup while the download itself is fine. # The run continues and says it cannot name its own revision, which is worth a warning rather than a refusal. + # This fallback is deliberate and gates no mutation: download_tree falls back to fetching $REF by name when RESOLVED is empty, exactly as it would if resolve_ref did not exist, and it has its own die on a real download failure. warn "Could not resolve $REF to a commit, so this run cannot be attributed to one" RESOLVED="" return 0 diff --git a/host-setup/linux/install-tools.sh b/host-setup/linux/install-tools.sh index 54fc22c3..0a4ecb0b 100755 --- a/host-setup/linux/install-tools.sh +++ b/host-setup/linux/install-tools.sh @@ -241,8 +241,13 @@ apt_install_displacing() { return 0 fi + # The simulation is what removals are previewed from, so a simulation that fails to run at all must not read the same as a simulation that ran and found nothing to remove. + local sim + sim=$(apt-get -s install "$package" 2>&1) || + die "apt-get -s install $package failed, so removals cannot be previewed before the real install runs: $sim" + local -a removals=() - readarray -t removals < <(apt-get -s install "$package" 2>/dev/null | awk '/^Remv / { print $2 }') + readarray -t removals < <(awk '/^Remv / { print $2 }' <<<"$sim") if [[ ${#removals[@]} -gt 0 ]]; then log " Installing $package removes ${#removals[@]} package(s): ${removals[*]}" log " Their dependencies are left installed, for a later apt autoremove to clean up" @@ -1410,11 +1415,20 @@ configure_sudo_timestamp() { "${SUDO[@]}" cmp -s "$staged" "$SUDOERS_FILE" 2>/dev/null && own_current=true # Another file setting either option is named rather than merged into, since which one wins is the order sudo reads them in and not something this can decide. - local elsewhere + local elsewhere status=0 # A name holding a dot or ending in a tilde is one sudo skips, this run's own staged file included, so a setting in it is an override sudo never reads. - elsewhere=$("${SUDO[@]}" grep -rnsE '^[[:space:]]*Defaults.*timestamp_(type|timeout)' \ - --exclude='*.*' --exclude='*~' --exclude="${SUDOERS_FILE##*/}" \ - /etc/sudoers /etc/sudoers.d 2>/dev/null) || elsewhere="" + # Grep's own "no match" exit (1) is folded to 0 inside the privileged shell, so the status sudo hands back distinguishes only "sudo could not even run this" from "the scan ran", never grep's ordinary no-match case from a sudo failure that also happens to exit 1. + # shellcheck disable=SC2016 # $1/$2 are meant for the inner sh -c script, not this outer shell. + elsewhere=$("${SUDO[@]}" sh -c ' + out=$(grep -rnsE "$1" --exclude="*.*" --exclude="*~" --exclude="$2" /etc/sudoers /etc/sudoers.d 2>&1) + rc=$? + printf %s "$out" + [ "$rc" -eq 1 ] && exit 0 + exit "$rc" + ' _ '^[[:space:]]*Defaults.*timestamp_(type|timeout)' "${SUDOERS_FILE##*/}") || status=$? + if [[ $status -ne 0 ]]; then + die "Scanning /etc/sudoers and /etc/sudoers.d for other timestamp_type/timestamp_timeout entries failed: $elsewhere" + fi # Only this user's own entry is ever a delete candidate; a different user's entry, or one with no user named at all, changes something beyond what this run was asked to change, so it is reported and left alone. local -a delete_files=() unsafe_files=() diff --git a/host-setup/linux/upgrade-host.sh b/host-setup/linux/upgrade-host.sh index 95abeb64..9792da8f 100755 --- a/host-setup/linux/upgrade-host.sh +++ b/host-setup/linux/upgrade-host.sh @@ -174,7 +174,15 @@ refresh_snaps() { } upgradable_count() { - apt list --upgradable 2>/dev/null | grep -c '/' || true + # A failed listing and a listing that genuinely found nothing upgradable both read as "no matches" through grep alone, so the two are told apart here rather than both printing 0. + # This only ever backs a status report, so the answer here is "unknown" rather than a die: nothing downstream mutates on the strength of this count. + local out status=0 + out=$(apt list --upgradable 2>/dev/null) || status=$? + if [[ $status -ne 0 ]]; then + printf 'unknown, apt list --upgradable failed (exit %s)' "$status" + return 0 + fi + printf '%s package(s), against the lists as they stand' "$(grep -c '/' <<<"$out" || true)" } # --- Reboot --- @@ -241,8 +249,12 @@ release_preconditions() { die "Held packages block a release upgrade, unhold them first: $held" fi - local audit - audit=$("${SUDO[@]}" dpkg --audit 2>/dev/null || true) + # A dpkg --audit that fails to run is not the same as one that runs and finds nothing, and only the second one clears the way into a release upgrade. + local audit status=0 + audit=$("${SUDO[@]}" dpkg --audit 2>&1) || status=$? + if [[ $status -ne 0 ]]; then + die "dpkg --audit failed to run (exit $status), so half-configured packages cannot be ruled out before a release upgrade: $audit" + fi if [[ -n $audit ]]; then die "dpkg reports half-configured packages, fix them first: $audit" fi @@ -547,7 +559,7 @@ release_summary() { status() { log "Host : $(host_description)" - log "Upgradable: $(upgradable_count) package(s), against the lists as they stand" + log "Upgradable: $(upgradable_count)" log "Release : $(release_summary)" report_reboot