mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-04 13:52:24 +00:00
chore(governance): record auto-update RETURN-trap contract and proof
Follow-up to 564c235e32 (fix(update): disarm perform_update RETURN trap after
it runs). That runtime change touches the deployment-installability subsystem,
so this commit adds the matching substantive contract section in
docs/release-control/v6/internal/subsystems/deployment-installability.md and
the registry-accepted deployment-script-runtime proof in
scripts/installtests/pulse_auto_update_test.go
(TestPerformUpdateDoesNotLeakReturnTrap). The pair is registered in
scripts/release_control/canonical_completion_history.json by the next commit.
Change-source: pulse-maintainer
This commit is contained in:
parent
564c235e32
commit
bb25e3ec03
2 changed files with 103 additions and 6 deletions
|
|
@ -1532,12 +1532,24 @@ artifact-selection behaviour.
|
|||
including the installer-exits-nonzero rollback branch. The generated
|
||||
`pulse-update.service` gates on `ExecCondition=systemctl is-active`, so a
|
||||
service left stopped also silently disables every future unattended run.
|
||||
This is enforced by a `service_was_active`-guarded restart in each rollback
|
||||
branch plus the `ensure_service_restarted` RETURN-trap backstop, and pinned
|
||||
by `scripts/installtests/pulse_auto_update_test.go`
|
||||
(`TestPerformUpdateRestartsServiceWhenInstallerFails`,
|
||||
`TestEnsureServiceRestartedHonorsPriorServiceState`) and
|
||||
`scripts/tests/test-pulse-auto-update.sh`. For the same reason, root
|
||||
This is enforced by a `service_was_active`-guarded restart in each rollback
|
||||
branch plus the `ensure_service_restarted` RETURN-trap backstop, and pinned
|
||||
by `scripts/installtests/pulse_auto_update_test.go`
|
||||
(`TestPerformUpdateRestartsServiceWhenInstallerFails`,
|
||||
`TestEnsureServiceRestartedHonorsPriorServiceState`) and
|
||||
`scripts/tests/test-pulse-auto-update.sh`. The `ensure_service_restarted`
|
||||
RETURN-trap backstop must disarm itself with `trap - RETURN` on its first
|
||||
invocation (#2128): a RETURN trap is not scoped to `perform_update`, so
|
||||
leaving it installed re-runs it when a later function returns, and the
|
||||
function-local `installer_tmp`/`signature_tmp` are gone by then — under
|
||||
`set -u` the updater then aborts with `installer_tmp: unbound variable`
|
||||
after a successful update, failing `pulse-update.service` even though the
|
||||
install and version verification succeeded. Both trap installs (the early
|
||||
service-only trap and the tempfile trap) must disarm, and the tempfile trap
|
||||
must expand `${installer_tmp:-}`/`${signature_tmp:-}` so a stray invocation
|
||||
cannot abort the script. Pinned by
|
||||
`scripts/installtests/pulse_auto_update_test.go`
|
||||
(`TestPerformUpdateDoesNotLeakReturnTrap`). For the same reason, root
|
||||
`install.sh` writes outside the hardened update unit's writable set
|
||||
(`ProtectSystem=strict` with `ReadWritePaths` covering the install dir,
|
||||
config dir, `/tmp`, the auto-update helper's directory and the unit
|
||||
|
|
|
|||
|
|
@ -525,3 +525,88 @@ echo "SERVICE:$SERVICE_UP"
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPerformUpdateDoesNotLeakReturnTrap asserts the #2128 regression: a
|
||||
// RETURN trap is not scoped to the function that installs it, so
|
||||
// perform_update's trap must disarm itself on its first invocation. If it
|
||||
// leaks, it re-runs when a later function returns; the function-local
|
||||
// installer_tmp/signature_tmp are gone by then, and under `set -u` the updater
|
||||
// aborts with "installer_tmp: unbound variable" after a *successful* update,
|
||||
// leaving pulse-update.service failed even though the install worked.
|
||||
func TestPerformUpdateDoesNotLeakReturnTrap(t *testing.T) {
|
||||
script := `
|
||||
set -uo pipefail
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
GITHUB_REPO="rcourtman/Pulse"
|
||||
INSTALL_DIR="$TMP/opt/pulse"
|
||||
CONFIG_DIR="$TMP/etc/pulse"
|
||||
mkdir -p "$INSTALL_DIR/bin" "$CONFIG_DIR"
|
||||
printf 'v5.1.24\n' > "$INSTALL_DIR/VERSION"
|
||||
printf '#!/usr/bin/env bash\necho v5.1.24\n' > "$INSTALL_DIR/bin/pulse"
|
||||
chmod +x "$INSTALL_DIR/bin/pulse"
|
||||
export INSTALL_DIR
|
||||
export FAKE_NEW_VERSION="v5.1.25"
|
||||
|
||||
log() { echo "[$1] ${*:2}"; }
|
||||
detect_service_name() { echo pulse; }
|
||||
get_current_version() { tr -d '\r\n' < "$INSTALL_DIR/VERSION"; }
|
||||
verify_release_signature() { return 0; }
|
||||
sleep() { :; }
|
||||
|
||||
# curl writes the installer / signature to the -o target; the fake installer
|
||||
# bumps VERSION to the new version, simulating a successful install.
|
||||
curl() {
|
||||
local out="" prev="" arg
|
||||
for arg in "$@"; do
|
||||
if [[ "$prev" == "-o" ]]; then out="$arg"; fi
|
||||
prev="$arg"
|
||||
done
|
||||
if [[ -n "$out" ]]; then
|
||||
case "$out" in
|
||||
*.sig.*) printf 'dummy-signature\n' > "$out" ;;
|
||||
*)
|
||||
cat > "$out" <<'INSTALLER'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "${FAKE_NEW_VERSION}" > "${PULSE_INSTALL_DIR}/VERSION"
|
||||
exit 0
|
||||
INSTALLER
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Pulse was running and stays running, so perform_update succeeds.
|
||||
systemctl() {
|
||||
if [[ "$1" == "is-active" ]]; then return 0; fi
|
||||
return 0
|
||||
}
|
||||
` + extractAutoUpdateFunction(t, "is_prerelease_tag") + `
|
||||
` + extractAutoUpdateFunction(t, "resolve_install_script_url") + `
|
||||
` + extractAutoUpdateFunction(t, "wait_for_service_active") + `
|
||||
` + extractAutoUpdateFunction(t, "ensure_service_restarted") + `
|
||||
` + extractAutoUpdateFunction(t, "perform_update") + `
|
||||
if perform_update v5.1.25; then echo "RESULT:succeeded"; else echo "RESULT:failed"; fi
|
||||
echo "LEAKED:$(trap -p RETURN)"
|
||||
# The real caller (main) returns after perform_update; a leaked trap would
|
||||
# re-run here with the locals gone.
|
||||
caller() { return 0; }
|
||||
caller
|
||||
echo "AFTER_CALLER"
|
||||
`
|
||||
|
||||
out, err := exec.Command("bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("bash: %v\n%s", err, out)
|
||||
}
|
||||
got := string(out)
|
||||
for _, want := range []string{"RESULT:succeeded", "LEAKED:\n", "AFTER_CALLER"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("missing %q in perform_update success output:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
if strings.Contains(got, "unbound variable") {
|
||||
t.Fatalf("perform_update leaked its RETURN trap; a later function return aborted under set -u:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue