From 440348cb14cd7a5ff889a8847321a4c00cf80afb Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:26:35 +0100 Subject: [PATCH] Keep audit report cleanup owned by the invoking shell The declared 80-case trace reproduced a stopped watchdog running its inherited EXIT cleanup before classification, deleting a captured critical finding. Guard cleanup by Bash subshell ownership, including Bash 3.2, and exercise the actual hook with a deterministic child-exit control and a rejected legacy hook. Retain parent cleanup and every existing audit verdict and deadline. Contract-Neutral: Repair captured-report lifetime only; advisory classification, strict and inherited verdicts, invocation scope, retry budgets and external permissions remain unchanged. Change-source: pulse-maintainer --- .../installtests/build_release_assets_test.go | 44 +++++++++++++++++++ scripts/npm-audit-retry.sh | 5 ++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 769abf7e28..438cfb1bb1 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -5131,3 +5131,47 @@ func TestNpmAuditNativeScopeArgumentsPreserveFindings(t *testing.T) { } } } + +func TestNpmAuditReportCleanupRemainsParentOwned(t *testing.T) { + runner, err := os.ReadFile(repoFile("scripts", "npm-audit-retry.sh")) + if err != nil { + t.Fatal(err) + } + hook := regexp.MustCompile(`(?m)^trap '.*' EXIT$`).FindString(string(runner)) + if hook == "" { + t.Fatal("audit report cleanup hook is missing") + } + for _, tc := range []struct { + name, hook string + status int + }{ + {"actual_parent_owned_hook", hook, 0}, + {"legacy_child_cleanup_rejected", `trap 'rm -f "${report_file}"' EXIT`, 23}, + } { + t.Run(tc.name, func(t *testing.T) { + path := filepath.Join(t.TempDir(), "report") + // Install the exact source hook in the child explicitly to model + // the early-signal window, without timing a background process. + // It must retain the report there, and still remove it at parent exit. + script := "report_file=\"$REPORT_FIXTURE\"\nprintf retained >\"$report_file\"\n" + tc.hook + + "\n(\n" + tc.hook + "\n)\ntest -f \"$report_file\" || exit 23\n" + cmd := exec.Command("bash", "-c", script) + cmd.Env = append(os.Environ(), "REPORT_FIXTURE="+path) + output, err := cmd.CombinedOutput() + status := 0 + if err != nil { + exit, ok := err.(*exec.ExitError) + if !ok { + t.Fatal(err) + } + status = exit.ExitCode() + } + if status != tc.status { + t.Fatalf("child cleanup verdict=%d want=%d: %s", status, tc.status, output) + } + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("invoking shell did not clean up its report: %v", err) + } + }) + } +} diff --git a/scripts/npm-audit-retry.sh b/scripts/npm-audit-retry.sh index 737f3ed250..094af0e333 100755 --- a/scripts/npm-audit-retry.sh +++ b/scripts/npm-audit-retry.sh @@ -230,7 +230,10 @@ else: } report_file="$(mktemp)" -trap 'rm -f "${report_file}"' EXIT +# A watchdog stopped before its subshell initialises can run this inherited +# EXIT trap. Only the invoking shell owns cleanup: the captured verdict must +# remain available after the watchdog is stopped, on Bash 3.2 as well as 5.x. +trap 'if [ "$BASH_SUBSHELL" -eq 0 ]; then rm -f "${report_file}"; fi' EXIT attempt=1 delay="${DELAY}"