From f364cfc73b6a2119fac328bc8fe7c225da3eb71c Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:48:13 +0100 Subject: [PATCH] Reset Podman qualification storage on teardown --- .../v6/internal/subsystems/agent-lifecycle.md | 10 +++-- .../subsystems/deployment-installability.md | 4 +- ...ure_runtime_rootless_qualification_test.go | 37 +++++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 9f81fa970..e7292a5b5 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -7347,10 +7347,12 @@ semantic-only report and racing the evidence assertion. Cleanup removes the exact fixture containers through both still-live runtime APIs before stopping their services or deleting storage, so Podman namespace and shared-memory mounts cannot outlive the state they protect. After both -runtime services and the delegated user manager stop, cleanup waits until the -kernel mount table contains no mount at or below any dedicated runtime state -root before deleting that state; an empty runtime inventory or a successful -systemd stop alone is not mount-release evidence. +runtime services stop, Podman cleanup resets each rootless and rootful storage +identity through the local runtime CLI before the delegated user manager is +stopped. Cleanup then waits until the kernel mount table contains no mount at +or below any dedicated runtime state root before deleting that state; an empty +runtime inventory or a successful systemd stop alone is not mount-release +evidence. The wrapper must compile every Go artifact with mandatory VCS stamping so a missing revision or unavailable clean-worktree proof fails before live evidence can be emitted; the receipt and validator retain the exact artifact hashes and diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 6c2f2487c..e2a21d7a2 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -269,7 +269,9 @@ Direct recovery is complete only after the full stable telemetry digest is observed; the first semantic-only report cannot satisfy the qualification wait. Final cleanup removes the exact rootless and rootful fixture containers while both runtime APIs are live, then stops the services and delegated user manager. -It deletes runtime state only after the kernel mount table proves that no mount +After the services stop, Podman cleanup uses the local runtime CLI to reset each +rootless and rootful storage identity before stopping the user manager. It +deletes runtime state only after the kernel mount table proves that no mount remains at or below any dedicated runtime root; container inventory and unit state alone cannot authorize storage deletion. The outer wrapper forgets a tracked container only after strict labeled removal diff --git a/scripts/installtests/secure_runtime_rootless_qualification_test.go b/scripts/installtests/secure_runtime_rootless_qualification_test.go index 8fb7689a7..30ae4a613 100644 --- a/scripts/installtests/secure_runtime_rootless_qualification_test.go +++ b/scripts/installtests/secure_runtime_rootless_qualification_test.go @@ -429,6 +429,7 @@ func TestSecureRuntimeRootlessQualification(t *testing.T) { rootlessQualRemoveFixtures(t, daemon) rootlessQualStopUnit(t, daemon.rootlessUnit) rootlessQualStopUnit(t, daemon.rootfulUnit) + rootlessQualResetRuntimeStorage(t, daemon) rootlessQualStopUserManager(t, daemon) rootlessQualWaitRuntimeMountsReleased(t, daemon, 30*time.Second) rootlessQualRemoveRuntimeState(t, daemon) @@ -1263,6 +1264,27 @@ func rootlessQualRemoveFixtures(t *testing.T, d rootlessQualDaemon) { rootlessQualRemoveFixtureSet(t, rootlessQualCLI(d, false)) } +func rootlessQualResetRuntimeStorage(t *testing.T, d rootlessQualDaemon) { + t.Helper() + for _, command := range rootlessQualRuntimeResetCommands(d) { + if len(command) == 0 { + t.Fatal("empty runtime storage reset command") + } + rootlessQualCommand(t, 60*time.Second, command[0], command[1:]...) + } +} + +func rootlessQualRuntimeResetCommands(d rootlessQualDaemon) [][]string { + if d.runtime != "podman" { + return nil + } + const cleanPath = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + return [][]string{ + {"runuser", "-u", "pulse-agent", "--", "env", "-i", "HOME=" + d.home, "XDG_RUNTIME_DIR=" + filepath.Join("/run/user", strconv.Itoa(d.uid)), cleanPath, "/usr/bin/podman", "system", "reset", "--force"}, + {"/usr/bin/env", "-i", cleanPath, "/usr/bin/podman", "system", "reset", "--force"}, + } +} + func rootlessQualRemoveFixtureSet(t *testing.T, cli []string) { t.Helper() if len(cli) == 0 { @@ -1625,6 +1647,21 @@ func TestRootlessQualificationMountReleaseUsesExactKernelPaths(t *testing.T) { } } +func TestRootlessQualificationPodmanResetsBothStorageIdentitiesLocally(t *testing.T) { + d := rootlessQualDaemon{runtime: "podman", uid: 996, home: "/var/lib/pulse-rootless"} + got := rootlessQualRuntimeResetCommands(d) + want := [][]string{ + {"runuser", "-u", "pulse-agent", "--", "env", "-i", "HOME=/var/lib/pulse-rootless", "XDG_RUNTIME_DIR=/run/user/996", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "/usr/bin/podman", "system", "reset", "--force"}, + {"/usr/bin/env", "-i", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "/usr/bin/podman", "system", "reset", "--force"}, + } + if !slices.EqualFunc(got, want, slices.Equal[[]string]) { + t.Fatalf("Podman reset commands = %q, want %q", got, want) + } + if commands := rootlessQualRuntimeResetCommands(rootlessQualDaemon{runtime: "docker"}); len(commands) != 0 { + t.Fatalf("Docker cleanup unexpectedly gained Podman reset commands: %q", commands) + } +} + func TestRootlessQualificationStopAcceptsCollectedTransientUnit(t *testing.T) { tempDir := t.TempDir() logPath := filepath.Join(tempDir, "systemctl.log")