From b7b0eb0b0138491f6b2ad9666d9d006589c23aff Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:00:47 +0100 Subject: [PATCH] Handle each DSM override failure before owned temporary cleanup The retained race refusals still left manual temporary files after the command-double change. Check write, permission and atomic link operations individually rather than relying on the grouped chain to reach cleanup. Keep concurrent files/directories and all credentials intact; the same mandatory race assertions remain. Matching Go1.26.9 is now actually observed and the complete installer suite remains required. Change-source: pulse-maintainer --- .../v6/internal/subsystems/agent-lifecycle.md | 4 +++- .../internal/subsystems/deployment-installability.md | 4 +++- scripts/install.sh | 10 +++++++++- scripts/installtests/agent_upstart_teardown.py | 3 +-- scripts/installtests/install_sh_test.go | 2 +- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index ed26cce2a7..a6ead91181 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -34,7 +34,9 @@ reload, exact unknown-job status and independent process absence establish retirement. On unconfirmed retirement it is restored only if the original path is still absent, leaving the manual override and all local recovery state; atomic non-replacement links refuse concurrently created files or directories, -including during restore; the staged recovery copy is retained on conflict. Only confirmed retirement removes the staged config and manual +including during restore; the staged recovery copy is retained on conflict. +Override write, permission and link operations each have explicit checked +failure handling and remove only their owned non-credential temporary file. Only confirmed retirement removes the staged config and manual before credential/state erasure. No credentials, arguments or config contents are returned by these observations. Existing same-instance URL, exact record, TLS, systemd/OpenRC/rc.d/SysV and state-directory ownership remain unchanged. diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 623499f6ab..f5848e8bd8 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -35,7 +35,9 @@ reload, exact unknown-job status and independent process absence establish retirement. On unconfirmed retirement it is restored only if the original path is still absent, leaving the manual override and all local recovery state; atomic non-replacement links refuse concurrently created files or directories, -including during restore; the staged recovery copy is retained on conflict. Only confirmed retirement removes the staged config and manual +including during restore; the staged recovery copy is retained on conflict. +Override write, permission and link operations each have explicit checked +failure handling and remove only their owned non-credential temporary file. Only confirmed retirement removes the staged config and manual before credential/state erasure. No credentials, arguments or config contents are returned by these observations. Existing same-instance URL, exact record, TLS, systemd/OpenRC/rc.d/SysV and state-directory ownership remain unchanged. diff --git a/scripts/install.sh b/scripts/install.sh index aadf922a3e..1b23e07aac 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1622,7 +1622,15 @@ quiesce_upstart_agent_for_removal() { pending=$(mktemp "${override}.XXXXXX") || return 1 # Link without replacement: an override created concurrently is not # ours to overwrite. The temporary file contains no credential. - if ! { printf 'manual\n' > "$pending" && chmod 644 "$pending" && ln -T -- "$pending" "$override"; }; then + if ! printf 'manual\n' > "$pending"; then + rm -f -- "$pending" + return 1 + fi + if ! chmod 644 "$pending"; then + rm -f -- "$pending" + return 1 + fi + if ! ln -T -- "$pending" "$override"; then rm -f -- "$pending" return 1 fi diff --git a/scripts/installtests/agent_upstart_teardown.py b/scripts/installtests/agent_upstart_teardown.py index 84f6ddf3aa..25175d1b8d 100644 --- a/scripts/installtests/agent_upstart_teardown.py +++ b/scripts/installtests/agent_upstart_teardown.py @@ -212,8 +212,7 @@ ln() { race_restore_config "$target" fi printf 'file:link\n' >> "$EVENTS" - # Capture the external exit explicitly: builtin command under errexit - # can otherwise terminate the wrapper before production handles failure. + # Preserve the actual external command exit for the production handler. if builtin command ln "$@"; then return 0; else return $?; fi } mv() { diff --git a/scripts/installtests/install_sh_test.go b/scripts/installtests/install_sh_test.go index 8be711af94..5053a551d5 100644 --- a/scripts/installtests/install_sh_test.go +++ b/scripts/installtests/install_sh_test.go @@ -8473,7 +8473,7 @@ func TestInstallSHUpstartTeardownRequiresObservedQuiescence(t *testing.T) { cmd := exec.CommandContext(ctx, "python3", repoFile("scripts", "installtests", "agent_upstart_teardown.py")) output, err := cmd.CombinedOutput() if err != nil { - t.Fatalf("DSM Upstart removal controls: %v\n%s", err, output) + t.Fatalf("DSM Upstart retirement and owned temporary cleanup controls: %v\n%s", err, output) } for _, name := range []string{ "test_refused_manager_precedes_remote_and_state_erasure",