From 66e818d3e400f771ee65795ddc5317fad41d53be Mon Sep 17 00:00:00 2001 From: Samuel K Date: Sun, 18 Oct 2026 12:46:30 -0600 Subject: [PATCH 1/2] refactor(ci): convert managed-policy e2e wrapper to Go --- .github/workflows/pr-ci.yml | 1 - hack/microsandbox/main.go | 28 +- hack/microsandbox/managed_policy_linux.go | 398 ++++++++++++++++++ hack/microsandbox/managed_policy_other.go | 28 ++ hack/microsandbox/managed_policy_test.go | 466 ++++++++++++++++++++++ hack/microsandbox/run.go | 26 +- hack/microsandbox/run_test.go | 43 +- hack/run-managed-policy-e2e.sh | 96 ----- 8 files changed, 980 insertions(+), 106 deletions(-) create mode 100644 hack/microsandbox/managed_policy_linux.go create mode 100644 hack/microsandbox/managed_policy_other.go create mode 100644 hack/microsandbox/managed_policy_test.go delete mode 100755 hack/run-managed-policy-e2e.sh diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 61e481d8dd..5befc04193 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -32,7 +32,6 @@ jobs: - 'e2e/**' - 'hack/microsandbox/**' - 'hack/provider_update_ci/**' - - 'hack/run-managed-policy-e2e.sh' - '.github/workflows/pr-ci.yml' - '.github/workflows/docker-buildkit-windows.yml' - 'hack/licenses/**' diff --git a/hack/microsandbox/main.go b/hack/microsandbox/main.go index 59762e1486..4ab924fa9c 100644 --- a/hack/microsandbox/main.go +++ b/hack/microsandbox/main.go @@ -11,10 +11,12 @@ import ( ) const ( - installLinuxCommand = "install-linux" - runSuiteCommand = "run" - verifyReportCommand = "verify-report" - buildPerformanceToolsCommand = "build-performance-tools" + installLinuxCommand = "install-linux" + runSuiteCommand = "run" + verifyReportCommand = "verify-report" + buildPerformanceToolsCommand = "build-performance-tools" + managedPolicyE2ECommand = "managed-policy-e2e" + managedPolicyNamespaceCommand = "managed-policy-namespace" ) func main() { @@ -25,10 +27,13 @@ func runCLI(args []string, stdout, stderr io.Writer) int { if len(args) == 0 { _, _ = fmt.Fprintln( stderr, - "usage: devsy-ci-microsandbox ", + "usage: devsy-ci-microsandbox ", ) return 2 } + if code, ok := runManagedPolicyCommand(args, stderr); ok { + return code + } switch args[0] { case installLinuxCommand: return installLinuxCLI(args[1:], stdout, stderr) @@ -44,6 +49,19 @@ func runCLI(args []string, stdout, stderr io.Writer) int { } } +// runManagedPolicyCommand dispatches the managed-policy commands and reports +// whether args[0] named one of them. +func runManagedPolicyCommand(args []string, stderr io.Writer) (int, bool) { + switch args[0] { + case managedPolicyE2ECommand: + return managedPolicyE2ECLI(args[1:], stderr), true + case managedPolicyNamespaceCommand: + return managedPolicyNamespaceCLI(args[1:], stderr), true + default: + return 0, false + } +} + func installLinuxCLI(args []string, stdout, stderr io.Writer) int { if len(args) != 0 || runtime.GOOS != "linux" { _, _ = fmt.Fprintln(stderr, "install-linux requires Linux and takes no arguments") diff --git a/hack/microsandbox/managed_policy_linux.go b/hack/microsandbox/managed_policy_linux.go new file mode 100644 index 0000000000..0df7280e64 --- /dev/null +++ b/hack/microsandbox/managed_policy_linux.go @@ -0,0 +1,398 @@ +//go:build linux + +package main + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + "syscall" +) + +const ( + managedPolicyMarker = "DEVSY_REQUIRE_MSB_MANAGED_POLICY_FIXTURE" + managedPolicyTestBinary = "./e2e.test" + managedPolicyEtcRoot = "/etc" + managedPolicyDirName = "microsandbox" + managedPolicyFileName = "managed.json" + managedPolicyFixturePfx = "devsy-managed-policy." + managedPolicyNamespaceID = "mount-namespace-id" + managedPolicyDocument = "{\"version\":1,\"overrides\":{}}\n" +) + +// managedPolicyOwner is the uid/gid required for fixture directories and the policy file. +type managedPolicyOwner struct { + uid uint32 + gid uint32 +} + +// managedPolicyRuntime isolates every privileged side effect so the invariants +// can be exercised without root or a real mount namespace. +type managedPolicyRuntime struct { + euid int + owner managedPolicyOwner + etcRoot string + mountNamespace func() (string, error) + mountOverlay func(target, options string) error + readMountInfo func() (string, error) + lookPath func(name string) (string, error) + executable func() (string, error) + replace func(path string, argv, env []string) error +} + +// managedPolicyNamespace is the state handed from the outer wrapper to the +// unshared namespace stage. +type managedPolicyNamespace struct { + root string + idFile string + parent string + current string + command []string +} + +func productionManagedPolicyRuntime() managedPolicyRuntime { + return managedPolicyRuntime{ + euid: os.Geteuid(), + owner: managedPolicyOwner{}, + etcRoot: managedPolicyEtcRoot, + mountNamespace: func() (string, error) { return os.Readlink("/proc/self/ns/mnt") }, + mountOverlay: func(target, options string) error { + return syscall.Mount("overlay", target, "overlay", 0, options) + }, + readMountInfo: func() (string, error) { + data, err := os.ReadFile("/proc/self/mountinfo") + return string(data), err + }, + lookPath: exec.LookPath, + executable: os.Executable, + replace: syscall.Exec, + } +} + +// managedPolicyE2ECLI runs stage one: validate inputs, create the fixture, and +// re-exec this binary under a private mount namespace. It only returns on error. +func managedPolicyE2ECLI(args []string, stderr io.Writer) int { + syscall.Umask(0o077) + return managedPolicyExit(stderr, runManagedPolicyE2E(productionManagedPolicyRuntime(), args)) +} + +// managedPolicyNamespaceCLI runs stage two inside the unshared mount namespace. +// It is invoked only by managedPolicyE2ECLI. +func managedPolicyNamespaceCLI(args []string, stderr io.Writer) int { + syscall.Umask(0o077) + return managedPolicyExit( + stderr, + runManagedPolicyNamespace(productionManagedPolicyRuntime(), args), + ) +} + +func managedPolicyExit(stderr io.Writer, err error) int { + if err != nil { + _, _ = fmt.Fprintln(stderr, err) + return 1 + } + return 0 +} + +func runManagedPolicyE2E(rt managedPolicyRuntime, args []string) error { + if err := checkManagedPolicyInputs(rt, args); err != nil { + return err + } + root, err := createManagedPolicyFixture(os.Getenv("RUNNER_TEMP"), rt.owner) + if err != nil { + return err + } + parent, err := rt.mountNamespace() + if err != nil { + return fmt.Errorf("read parent mount namespace: %w", err) + } + self, err := rt.executable() + if err != nil { + return fmt.Errorf("resolve managed-policy wrapper: %w", err) + } + unshare, err := rt.lookPath("unshare") + if err != nil { + return fmt.Errorf("find unshare: %w", err) + } + argv := []string{ + "unshare", + "--mount", + "--propagation", + "private", + "--", + self, + managedPolicyNamespaceCommand, + root, + filepath.Join(root, managedPolicyNamespaceID), + parent, + } + argv = append(argv, args...) + if err := rt.replace(unshare, argv, os.Environ()); err != nil { + return fmt.Errorf("exec unshare: %w", err) + } + return nil +} + +func checkManagedPolicyInputs(rt managedPolicyRuntime, args []string) error { + if rt.euid != 0 { + return errors.New("managed-policy fixture must run as root") + } + if os.Getenv(managedPolicyMarker) != "true" { + return errors.New("managed-policy E2E wrapper requires the CI expectation marker") + } + if !isExistingDirectory(os.Getenv("RUNNER_TEMP")) { + return errors.New("RUNNER_TEMP must name an existing directory") + } + if len(args) == 0 || args[0] != managedPolicyTestBinary { + return errors.New("expected the already-built ./e2e.test binary as the first argument") + } + return nil +} + +func isExistingDirectory(path string) bool { + if path == "" { + return false + } + info, err := os.Stat(path) + return err == nil && info.IsDir() +} + +// createManagedPolicyFixture creates /devsy-managed-policy.* with +// upper and work subdirectories, all owned by owner with mode 0700. +func createManagedPolicyFixture(runnerTemp string, owner managedPolicyOwner) (string, error) { + parent := strings.TrimRight(runnerTemp, "/") + if parent == "" { + parent = "/" + } + root, err := os.MkdirTemp(parent, managedPolicyFixturePfx) + if err != nil { + return "", fmt.Errorf("create managed-policy fixture: %w", err) + } + if err := os.Chmod(root, 0o700); err != nil { + return "", fmt.Errorf("chmod managed-policy fixture: %w", err) + } + upper, work := filepath.Join(root, "upper"), filepath.Join(root, "work") + for _, dir := range []string{upper, work} { + if err := os.Mkdir(dir, 0o700); err != nil { + return "", fmt.Errorf("create managed-policy fixture directory: %w", err) + } + } + for _, dir := range []string{root, upper, work} { + if err := verifyManagedPolicyDirectory(dir, owner); err != nil { + return "", err + } + } + return root, nil +} + +func verifyManagedPolicyDirectory(path string, owner managedPolicyOwner) error { + info, err := os.Stat(path) + if err != nil { + return fmt.Errorf("inspect managed-policy fixture directory %s: %w", path, err) + } + stat, ok := info.Sys().(*syscall.Stat_t) + if !ok || stat.Uid != owner.uid || stat.Gid != owner.gid || stat.Mode&0o7777 != 0o700 { + return fmt.Errorf( + "managed-policy fixture directory has unexpected ownership or mode: %s", + path, + ) + } + return nil +} + +func runManagedPolicyNamespace(rt managedPolicyRuntime, args []string) error { + if rt.euid != 0 { + return errors.New("managed-policy namespace must run as root") + } + ns, err := enterManagedPolicyNamespace(rt, args) + if err != nil { + return err + } + upper, work := filepath.Join(ns.root, "upper"), filepath.Join(ns.root, "work") + options := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", rt.etcRoot, upper, work) + if err := rt.mountOverlay(rt.etcRoot, options); err != nil { + return fmt.Errorf("mount overlay on %s: %w", rt.etcRoot, err) + } + if err := verifyOverlayMount(rt, upper, work); err != nil { + return err + } + if err := ensureManagedPolicyDirAbsent(rt.etcRoot); err != nil { + return err + } + policy, err := writeInitialManagedPolicy(rt.etcRoot, rt.owner) + if err != nil { + return err + } + env := managedPolicyChildEnvironment(os.Environ(), ns, policy) + if err := rt.replace(ns.command[0], ns.command, env); err != nil { + return fmt.Errorf("exec %s: %w", ns.command[0], err) + } + return nil +} + +// enterManagedPolicyNamespace validates the stage-two arguments, confirms the +// mount namespace changed, and records its id for the test process. +func enterManagedPolicyNamespace( + rt managedPolicyRuntime, + args []string, +) (managedPolicyNamespace, error) { + if len(args) < 4 { + return managedPolicyNamespace{}, errors.New( + "managed-policy-namespace requires ", + ) + } + ns := managedPolicyNamespace{root: args[0], idFile: args[1], parent: args[2], command: args[3:]} + current, err := rt.mountNamespace() + if err != nil { + return ns, fmt.Errorf("read mount namespace: %w", err) + } + if current == ns.parent { + return ns, errors.New("mount namespace did not change") + } + ns.current = current + if err := os.WriteFile(ns.idFile, []byte(current+"\n"), 0o600); err != nil { + return ns, fmt.Errorf("write mount namespace id: %w", err) + } + return ns, nil +} + +func verifyOverlayMount(rt managedPolicyRuntime, upper, work string) error { + mountInfo, err := rt.readMountInfo() + if err != nil { + return fmt.Errorf("read mountinfo: %w", err) + } + if !hasOverlayMount(mountInfo, rt.etcRoot, upper, work) { + return errors.New( + "private /etc overlay does not use this fixture's owned upper and work directories", + ) + } + return nil +} + +// hasOverlayMount reports whether a mountinfo line shows an overlay at mountPoint +// whose super options name exactly the upper and work directories. +func hasOverlayMount(mountInfo, mountPoint, upper, work string) bool { + for line := range strings.SplitSeq(mountInfo, "\n") { + if overlayLineMatches(strings.Fields(line), mountPoint, upper, work) { + return true + } + } + return false +} + +// overlayLineMatches reports whether one mountinfo line's overlay options name +// the given upper and work directories. +func overlayLineMatches(fields []string, mountPoint, upper, work string) bool { + if len(fields) < 5 || fields[4] != mountPoint { + return false + } + sep := lastSeparatorIndex(fields) + if sep < 0 || sep+3 >= len(fields) || fields[sep+1] != "overlay" { + return false + } + options := strings.Split(fields[sep+3], ",") + return slices.Contains(options, "upperdir="+upper) && slices.Contains(options, "workdir="+work) +} + +func lastSeparatorIndex(fields []string) int { + sep := -1 + for i, field := range fields { + if field == "-" { + sep = i + } + } + return sep +} + +func ensureManagedPolicyDirAbsent(etcRoot string) error { + _, err := os.Lstat(filepath.Join(etcRoot, managedPolicyDirName)) + if err == nil { + return errors.New( + "managed-policy fixture requires /etc/microsandbox to be absent in the overlay lower layer", + ) + } + if errors.Is(err, fs.ErrNotExist) { + return nil + } + return fmt.Errorf("inspect /etc/microsandbox: %w", err) +} + +// writeInitialManagedPolicy creates /microsandbox/managed.json with the +// empty version-1 policy, written via an exclusive temp file and renamed into place. +func writeInitialManagedPolicy(etcRoot string, owner managedPolicyOwner) (string, error) { + dir := filepath.Join(etcRoot, managedPolicyDirName) + if err := os.Mkdir(dir, 0o755); err != nil { + return "", fmt.Errorf("create %s: %w", dir, err) + } + if err := setManagedPolicyOwner(dir, owner, 0o755); err != nil { + return "", err + } + tmp := filepath.Join(dir, fmt.Sprintf(".managed.json.initial.%d", os.Getpid())) + if err := createManagedPolicyTemp(tmp); err != nil { + return "", fmt.Errorf("write initial managed policy: %w", err) + } + if err := setManagedPolicyOwner(tmp, owner, 0o600); err != nil { + return "", err + } + policy := filepath.Join(dir, managedPolicyFileName) + if err := os.Rename(tmp, policy); err != nil { + return "", fmt.Errorf("install managed policy: %w", err) + } + return policy, nil +} + +func createManagedPolicyTemp(path string) error { + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) + if err != nil { + return err + } + _, writeErr := file.WriteString(managedPolicyDocument) + return errors.Join(writeErr, file.Close()) +} + +func setManagedPolicyOwner(path string, owner managedPolicyOwner, mode os.FileMode) error { + if err := os.Chown(path, int(owner.uid), int(owner.gid)); err != nil { + return fmt.Errorf("chown %s: %w", path, err) + } + if err := os.Chmod(path, mode); err != nil { + return fmt.Errorf("chmod %s: %w", path, err) + } + return nil +} + +// managedPolicyChildEnvironment returns environ with every managed-policy key +// replaced by the values the e2e test binary's consumer expects. +func managedPolicyChildEnvironment( + environ []string, + ns managedPolicyNamespace, + policy string, +) []string { + overrides := []string{ + managedPolicyMarker + "=true", + "DEVSY_TEST_MSB_MANAGED_POLICY=" + policy, + "DEVSY_TEST_MSB_FIXTURE_ROOT=" + ns.root, + "DEVSY_TEST_MSB_PARENT_MNT_NS=" + ns.parent, + "DEVSY_TEST_MSB_EXPECTED_MNT_NS=" + ns.current, + "DEVSY_TEST_MSB_EXPECTED_UPPER=" + filepath.Join(ns.root, "upper"), + "DEVSY_TEST_MSB_EXPECTED_WORK=" + filepath.Join(ns.root, "work"), + } + managed := make(map[string]bool, len(overrides)) + for _, assignment := range overrides { + key, _, _ := strings.Cut(assignment, "=") + managed[key] = true + } + env := make([]string, 0, len(environ)+len(overrides)) + for _, entry := range environ { + key, _, _ := strings.Cut(entry, "=") + if !managed[key] { + env = append(env, entry) + } + } + return append(env, overrides...) +} diff --git a/hack/microsandbox/managed_policy_other.go b/hack/microsandbox/managed_policy_other.go new file mode 100644 index 0000000000..6e7a09c87f --- /dev/null +++ b/hack/microsandbox/managed_policy_other.go @@ -0,0 +1,28 @@ +//go:build !linux + +package main + +import ( + "fmt" + "io" + "runtime" +) + +// managedPolicyE2ECLI rejects the wrapper off Linux, which needs mount namespaces and overlayfs. +func managedPolicyE2ECLI(_ []string, stderr io.Writer) int { + return managedPolicyUnsupported(stderr) +} + +// managedPolicyNamespaceCLI rejects the internal namespace stage off Linux. +func managedPolicyNamespaceCLI(_ []string, stderr io.Writer) int { + return managedPolicyUnsupported(stderr) +} + +func managedPolicyUnsupported(stderr io.Writer) int { + _, _ = fmt.Fprintf( + stderr, + "managed-policy-e2e is unsupported on %s: requires Linux mount namespaces and overlayfs\n", + runtime.GOOS, + ) + return 2 +} diff --git a/hack/microsandbox/managed_policy_test.go b/hack/microsandbox/managed_policy_test.go new file mode 100644 index 0000000000..983ac1c1af --- /dev/null +++ b/hack/microsandbox/managed_policy_test.go @@ -0,0 +1,466 @@ +//go:build linux + +package main + +import ( + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "testing" +) + +const ( + managedPolicyTrue = "true" + managedPolicyParentNS = "mnt:[parent]" + managedPolicyChildNS = "mnt:[child]" + managedPolicyTestArg = "-test.v" + managedPolicyEnvPath = "PATH=/bin" + managedPolicyNotDirMsg = "RUNNER_TEMP must name an existing directory" + managedPolicyBinaryMsg = "expected the already-built ./e2e.test binary as the first argument" + managedPolicyAbsentMsg = "managed-policy fixture requires /etc/microsandbox to be absent in the overlay lower layer" +) + +type managedPolicyFake struct { + current string + overlayTarget string + overlayOptions string + mountInfo string + replaced bool + path string + argv []string + env []string +} + +// newManagedPolicyFake returns a runtime whose privileged calls are recorded +// instead of performed. Ownership is the test process's own uid/gid so the +// fixture checks pass without root. +func newManagedPolicyFake(euid int, etcRoot string) (managedPolicyRuntime, *managedPolicyFake) { + fake := &managedPolicyFake{current: managedPolicyChildNS} + rt := managedPolicyRuntime{ + euid: euid, + owner: managedPolicyOwner{uid: uint32(os.Getuid()), gid: uint32(os.Getgid())}, + etcRoot: etcRoot, + mountNamespace: func() (string, error) { + return fake.current, nil + }, + mountOverlay: func(target, options string) error { + fake.overlayTarget, fake.overlayOptions = target, options + return nil + }, + readMountInfo: func() (string, error) { + if fake.mountInfo != "" { + return fake.mountInfo, nil + } + return fmt.Sprintf( + "36 25 0:32 / %s rw - overlay overlay %s", + fake.overlayTarget, + fake.overlayOptions, + ), nil + }, + lookPath: func(string) (string, error) { return "/usr/bin/unshare", nil }, + executable: func() (string, error) { + return "/abs/devsy", nil + }, + replace: func(path string, argv, env []string) error { + fake.replaced, fake.path, fake.argv, fake.env = true, path, argv, env + return nil + }, + } + return rt, fake +} + +func requireManagedPolicyMode(t *testing.T, path string, want os.FileMode) { + t.Helper() + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if got := info.Mode().Perm(); got != want { + t.Errorf("%s mode = %o, want %o", path, got, want) + } +} + +type managedPolicyInputCase struct { + name string + euid int + marker string + runnerTemp string + args []string + want string +} + +func managedPolicyInputCases(validTemp string) []managedPolicyInputCase { + binary := []string{managedPolicyTestBinary} + return []managedPolicyInputCase{ + { + name: "non-root", euid: 1000, marker: managedPolicyTrue, runnerTemp: validTemp, + args: binary, want: "managed-policy fixture must run as root", + }, + { + name: "missing marker", euid: 0, marker: "", runnerTemp: validTemp, + args: binary, want: "managed-policy E2E wrapper requires the CI expectation marker", + }, + { + name: "empty runner temp", euid: 0, marker: managedPolicyTrue, runnerTemp: "", + args: binary, want: managedPolicyNotDirMsg, + }, + { + name: "runner temp absent", + euid: 0, + marker: managedPolicyTrue, + runnerTemp: filepath.Join( + validTemp, + "absent", + ), + args: binary, + want: managedPolicyNotDirMsg, + }, + { + name: "wrong binary", euid: 0, marker: managedPolicyTrue, runnerTemp: validTemp, + args: []string{"./other"}, want: managedPolicyBinaryMsg, + }, + { + name: "no arguments", euid: 0, marker: managedPolicyTrue, runnerTemp: validTemp, + want: managedPolicyBinaryMsg, + }, + } +} + +func TestManagedPolicyE2EInputInvariants(t *testing.T) { + for _, tc := range managedPolicyInputCases(t.TempDir()) { + t.Run(tc.name, func(t *testing.T) { + t.Setenv(managedPolicyMarker, tc.marker) + t.Setenv("RUNNER_TEMP", tc.runnerTemp) + rt, fake := newManagedPolicyFake(tc.euid, t.TempDir()) + err := runManagedPolicyE2E(rt, tc.args) + if err == nil || err.Error() != tc.want { + t.Fatalf("runManagedPolicyE2E() error = %v, want %q", err, tc.want) + } + if fake.replaced { + t.Fatal("re-exec happened despite invalid input") + } + }) + } +} + +func TestManagedPolicyE2EReexecsWithFixtureUnderRunnerTemp(t *testing.T) { + runnerTemp := t.TempDir() + t.Setenv(managedPolicyMarker, managedPolicyTrue) + t.Setenv("RUNNER_TEMP", runnerTemp) + rt, fake := newManagedPolicyFake(0, t.TempDir()) + fake.current = managedPolicyParentNS + + if err := runManagedPolicyE2E( + rt, + []string{managedPolicyTestBinary, managedPolicyTestArg}, + ); err != nil { + t.Fatal(err) + } + if !fake.replaced { + t.Fatal("runManagedPolicyE2E() did not re-exec") + } + if len(fake.argv) < 8 { + t.Fatalf("argv = %#v, too short", fake.argv) + } + root := fake.argv[7] + assertManagedPolicyFixtureRoot(t, root, runnerTemp) + want := []string{ + "unshare", "--mount", "--propagation", "private", "--", + "/abs/devsy", "managed-policy-namespace", root, filepath.Join(root, "mount-namespace-id"), + managedPolicyParentNS, managedPolicyTestBinary, managedPolicyTestArg, + } + if fake.path != "/usr/bin/unshare" || !slices.Equal(fake.argv, want) { + t.Fatalf("exec path = %q argv = %#v, want /usr/bin/unshare %#v", fake.path, fake.argv, want) + } +} + +func assertManagedPolicyFixtureRoot(t *testing.T, root, runnerTemp string) { + t.Helper() + if filepath.Dir(root) != runnerTemp || + !strings.HasPrefix(filepath.Base(root), "devsy-managed-policy.") { + t.Fatalf( + "fixture root %q is not a devsy-managed-policy directory under RUNNER_TEMP %q", + root, + runnerTemp, + ) + } + for _, dir := range []string{root, filepath.Join(root, "upper"), filepath.Join(root, "work")} { + requireManagedPolicyMode(t, dir, 0o700) + } +} + +func TestCreateManagedPolicyFixtureRejectsUnexpectedOwner(t *testing.T) { + runnerTemp := t.TempDir() + wrongOwner := managedPolicyOwner{uid: uint32(os.Getuid()) + 1, gid: uint32(os.Getgid())} + _, err := createManagedPolicyFixture(runnerTemp, wrongOwner) + if err == nil || + !strings.HasPrefix( + err.Error(), + "managed-policy fixture directory has unexpected ownership or mode: ", + ) { + t.Fatalf("createManagedPolicyFixture() error = %v, want ownership failure", err) + } +} + +type managedPolicyNamespaceCase struct { + name string + euid int + args []string + setup func(t *testing.T, etc string, fake *managedPolicyFake) + want string +} + +func mkExistingPolicyDir(t *testing.T, etc string, _ *managedPolicyFake) { + t.Helper() + if err := os.Mkdir(filepath.Join(etc, "microsandbox"), 0o755); err != nil { + t.Fatal(err) + } +} + +func linkDanglingPolicy(t *testing.T, etc string, _ *managedPolicyFake) { + t.Helper() + if err := os.Symlink("/nonexistent/target", filepath.Join(etc, "microsandbox")); err != nil { + t.Fatal(err) + } +} + +func setParentNamespace(_ *testing.T, _ string, fake *managedPolicyFake) { + fake.current = managedPolicyParentNS +} + +func setForeignOverlay(_ *testing.T, _ string, fake *managedPolicyFake) { + fake.mountInfo = "36 25 0:32 / /etc rw - overlay overlay " + + "lowerdir=/etc,upperdir=/elsewhere/upper,workdir=/elsewhere/work" +} + +func managedPolicyNamespaceCases() []managedPolicyNamespaceCase { + return []managedPolicyNamespaceCase{ + {name: "non-root", euid: 1000, want: "managed-policy namespace must run as root"}, + { + name: "too few arguments", + euid: 0, + args: []string{"root", "id", managedPolicyParentNS}, + want: "managed-policy-namespace requires ", + }, + { + name: "namespace unchanged", + euid: 0, + setup: setParentNamespace, + want: "mount namespace did not change", + }, + { + name: "mountinfo names other directories", + euid: 0, + setup: setForeignOverlay, + want: "private /etc overlay does not use this fixture's owned upper and work directories", + }, + { + name: "existing policy directory", + euid: 0, + setup: mkExistingPolicyDir, + want: managedPolicyAbsentMsg, + }, + { + name: "dangling policy symlink", + euid: 0, + setup: linkDanglingPolicy, + want: managedPolicyAbsentMsg, + }, + } +} + +func TestManagedPolicyNamespaceInvariants(t *testing.T) { + for _, tc := range managedPolicyNamespaceCases() { + t.Run(tc.name, func(t *testing.T) { + etc := t.TempDir() + root := t.TempDir() + rt, fake := newManagedPolicyFake(tc.euid, etc) + if tc.setup != nil { + tc.setup(t, etc, fake) + } + args := tc.args + if args == nil { + args = []string{ + root, + filepath.Join(root, "id"), + managedPolicyParentNS, + managedPolicyTestBinary, + } + } + err := runManagedPolicyNamespace(rt, args) + if err == nil || err.Error() != tc.want { + t.Fatalf("runManagedPolicyNamespace() error = %v, want %q", err, tc.want) + } + if fake.replaced { + t.Fatal("namespace stage executed the command despite failing invariant") + } + }) + } +} + +func TestManagedPolicyNamespaceInstallsPolicyAndExecsCommand(t *testing.T) { + etc := t.TempDir() + root := t.TempDir() + t.Setenv("DEVSY_TEST_MSB_FIXTURE_ROOT", "/stale") + rt, fake := newManagedPolicyFake(0, etc) + idFile := filepath.Join(root, "mount-namespace-id") + + args := []string{ + root, + idFile, + managedPolicyParentNS, + managedPolicyTestBinary, + managedPolicyTestArg, + } + if err := runManagedPolicyNamespace(rt, args); err != nil { + t.Fatal(err) + } + assertManagedPolicyNamespaceID(t, idFile) + assertManagedPolicyOverlay(t, fake, etc, root) + assertManagedPolicyInstalled(t, etc) + assertManagedPolicyExec(t, fake, etc, root) +} + +func assertManagedPolicyNamespaceID(t *testing.T, idFile string) { + t.Helper() + id, err := os.ReadFile(idFile) + if err != nil || string(id) != managedPolicyChildNS+"\n" { + t.Fatalf("namespace id = %q, err = %v", id, err) + } + requireManagedPolicyMode(t, idFile, 0o600) +} + +func assertManagedPolicyOverlay(t *testing.T, fake *managedPolicyFake, etc, root string) { + t.Helper() + wantOptions := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", + etc, filepath.Join(root, "upper"), filepath.Join(root, "work")) + if fake.overlayTarget != etc || fake.overlayOptions != wantOptions { + t.Fatalf( + "overlay = %q %q, want %q %q", + fake.overlayTarget, + fake.overlayOptions, + etc, + wantOptions, + ) + } +} + +func assertManagedPolicyInstalled(t *testing.T, etc string) { + t.Helper() + policyDir := filepath.Join(etc, "microsandbox") + policy := filepath.Join(policyDir, "managed.json") + data, err := os.ReadFile(policy) + if err != nil || string(data) != managedPolicyDocument { + t.Fatalf("policy = %q, err = %v", data, err) + } + requireManagedPolicyMode(t, policy, 0o600) + requireManagedPolicyMode(t, policyDir, 0o755) + entries, err := os.ReadDir(policyDir) + if err != nil || len(entries) != 1 { + t.Fatalf("policy dir entries = %v, err = %v; want only managed.json", entries, err) + } +} + +func assertManagedPolicyExec(t *testing.T, fake *managedPolicyFake, etc, root string) { + t.Helper() + wantArgv := []string{managedPolicyTestBinary, managedPolicyTestArg} + if !fake.replaced || fake.path != managedPolicyTestBinary || + !slices.Equal(fake.argv, wantArgv) { + t.Fatalf("exec path = %q argv = %#v", fake.path, fake.argv) + } + if slices.Contains(fake.env, "DEVSY_TEST_MSB_FIXTURE_ROOT=/stale") { + t.Fatal("stale DEVSY_TEST_MSB_FIXTURE_ROOT leaked into child environment") + } + policy := filepath.Join(etc, "microsandbox", "managed.json") + for _, want := range []string{ + "DEVSY_REQUIRE_MSB_MANAGED_POLICY_FIXTURE=" + managedPolicyTrue, + "DEVSY_TEST_MSB_MANAGED_POLICY=" + policy, + "DEVSY_TEST_MSB_FIXTURE_ROOT=" + root, + "DEVSY_TEST_MSB_EXPECTED_UPPER=" + filepath.Join(root, "upper"), + } { + if !slices.Contains(fake.env, want) { + t.Errorf("child environment missing %q", want) + } + } +} + +func TestHasOverlayMount(t *testing.T) { + const line = "36 25 0:32 / /etc rw,relatime shared:1 - overlay overlay " + + "rw,lowerdir=/etc,upperdir=/s/upper,workdir=/s/work" + cases := []struct { + name string + mountInfo string + want bool + }{ + {name: "matching overlay", mountInfo: line, want: true}, + { + name: "upper and work on different lines", + mountInfo: "36 25 0:32 / /etc rw - overlay overlay upperdir=/s/upper\n" + + "37 25 0:33 / /etc rw - overlay overlay workdir=/s/work", + want: false, + }, + { + name: "other mount point", + mountInfo: strings.Replace(line, " /etc ", " /usr ", 1), + want: false, + }, + { + name: "not an overlay", + mountInfo: strings.Replace(line, "overlay overlay", "tmpfs tmpfs", 1), + want: false, + }, + { + name: "different upper path", + mountInfo: strings.Replace(line, "/s/upper", "/x/upper", 1), + want: false, + }, + {name: "truncated line", mountInfo: "36 25 /etc", want: false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := hasOverlayMount(tc.mountInfo, "/etc", "/s/upper", "/s/work"); got != tc.want { + t.Fatalf("hasOverlayMount() = %t, want %t", got, tc.want) + } + }) + } +} + +func countEnvPrefix(env []string, prefix string) int { + count := 0 + for _, entry := range env { + if strings.HasPrefix(entry, prefix) { + count++ + } + } + return count +} + +func TestManagedPolicyChildEnvironmentReplacesManagedKeys(t *testing.T) { + environ := []string{ + managedPolicyEnvPath, + "RUNNER_TEMP=/rt", + "DEVSY_TEST_MSB_FIXTURE_ROOT=/stale", + "DEVSY_TEST_MSB_EXPECTED_UPPER=/stale", + } + ns := managedPolicyNamespace{root: "/r"} + env := managedPolicyChildEnvironment(environ, ns, "/etc/microsandbox/managed.json") + + for _, keep := range []string{managedPolicyEnvPath, "RUNNER_TEMP=/rt"} { + if !slices.Contains(env, keep) { + t.Errorf("environment dropped %q", keep) + } + } + for _, stale := range []string{"DEVSY_TEST_MSB_FIXTURE_ROOT=/stale", "DEVSY_TEST_MSB_EXPECTED_UPPER=/stale"} { + if slices.Contains(env, stale) { + t.Errorf("environment kept stale %q", stale) + } + } + if !slices.Contains(env, "DEVSY_TEST_MSB_FIXTURE_ROOT=/r") { + t.Error("environment missing fixture root override") + } + if n := countEnvPrefix(env, "DEVSY_TEST_MSB_EXPECTED_UPPER="); n != 1 { + t.Fatalf("DEVSY_TEST_MSB_EXPECTED_UPPER entries = %d, want 1", n) + } +} diff --git a/hack/microsandbox/run.go b/hack/microsandbox/run.go index 9f863447a5..41151364fd 100644 --- a/hack/microsandbox/run.go +++ b/hack/microsandbox/run.go @@ -93,6 +93,11 @@ func executeSuite( _, _ = fmt.Fprintf(output.stderr, "::error::%v\n", err) return suiteExecution{testStatus: 2} } + prefix, err := suiteCommandPrefix(cfg.label) + if err != nil { + _, _ = fmt.Fprintf(output.stderr, "::error::%v\n", err) + return suiteExecution{testStatus: 2} + } reportPath := filepath.Join(cfg.tempDir, "microsandbox-"+cfg.label+".json") logPath := filepath.Join(cfg.tempDir, "microsandbox-"+cfg.label+".log") @@ -111,9 +116,7 @@ func executeSuite( "--ginkgo.label-filter=" + cfg.label, "-ginkgo.json-report=" + reportPath, } - if cfg.label == managedPolicyLabel { - commandArgs = append([]string{"../hack/run-managed-policy-e2e.sh"}, commandArgs...) - } + commandArgs = append(prefix, commandArgs...) args = append(args, commandArgs...) cmd := exec.CommandContext(ctx, "sudo", args...) cmd.Dir = filepath.Join(cfg.repositoryDir, "e2e") @@ -181,6 +184,23 @@ func isValidSuiteLabel(label string) bool { return suiteLabelPattern.MatchString(label) } +// currentExecutable resolves this binary for the managed-policy re-exec. It is a +// variable so tests can make resolution fail. +var currentExecutable = os.Executable + +// suiteCommandPrefix returns the argv prefix that routes the managed-policy suite +// through this binary's managed-policy-e2e subcommand. Other labels get no prefix. +func suiteCommandPrefix(label string) ([]string, error) { + if label != managedPolicyLabel { + return nil, nil + } + exe, err := currentExecutable() + if err != nil { + return nil, fmt.Errorf("resolve managed-policy wrapper: %w", err) + } + return []string{exe, managedPolicyE2ECommand}, nil +} + func suiteSudoArguments(cfg suiteConfig) ([]string, error) { values, err := requiredSuiteEnvironment() if err != nil { diff --git a/hack/microsandbox/run_test.go b/hack/microsandbox/run_test.go index 9c8fa884d1..96c9d1e12d 100644 --- a/hack/microsandbox/run_test.go +++ b/hack/microsandbox/run_test.go @@ -116,7 +116,16 @@ func TestRunSuiteManagedPolicyUsesWrapperAndFixtureEnvironment(t *testing.T) { ); code != 0 { t.Fatalf("runSuite() = %d; stderr=%s", code, stderr.String()) } - if len(commands) != 2 || !stringArgsContain(commands[0], "../hack/run-managed-policy-e2e.sh") { + assertManagedPolicyInvocation(t, commands, tempDir) +} + +func assertManagedPolicyInvocation(t *testing.T, commands [][]string, tempDir string) { + t.Helper() + exe, exeErr := os.Executable() + if exeErr != nil { + t.Fatal(exeErr) + } + if len(commands) != 2 || !isManagedPolicyInvocation(commands[0], exe) { t.Fatalf("managed invocation = %#v", commands) } for _, want := range []string{ @@ -128,6 +137,38 @@ func TestRunSuiteManagedPolicyUsesWrapperAndFixtureEnvironment(t *testing.T) { } } +// isManagedPolicyInvocation reports whether argv runs exe with the managed-policy +// wrapper command followed by ./e2e.test. +func isManagedPolicyInvocation(argv []string, exe string) bool { + index := slices.Index(argv, exe) + return index >= 0 && index+2 < len(argv) && + argv[index+1] == managedPolicyE2ECommand && argv[index+2] == "./e2e.test" +} + +func TestRunSuiteManagedPolicyReportsExecutableResolutionFailureBeforeInvocation(t *testing.T) { + cfg, _ := newSuiteRunFixture(t, managedPolicyLabel) + original := currentExecutable + currentExecutable = func() (string, error) { return "", errors.New("no executable path") } + t.Cleanup(func() { currentExecutable = original }) + var stdout, stderr bytes.Buffer + invoked := false + run := func(*exec.Cmd) error { + invoked = true + return nil + } + if code := runSuite( + context.Background(), + cfg, + run, + commandOutput{stdout: &stdout, stderr: &stderr}, + ); code != 2 { + t.Fatalf("runSuite() = %d, want 2; stderr=%s", code, stderr.String()) + } + if invoked { + t.Fatal("managed-policy command ran after executable resolution failed") + } +} + func TestRunSuitePreservesNativeExitPrecedenceAndArtifacts(t *testing.T) { cfg, tempDir := newSuiteRunFixture(t, "up-provider-microsandbox-images") var console, stderr bytes.Buffer diff --git a/hack/run-managed-policy-e2e.sh b/hack/run-managed-policy-e2e.sh deleted file mode 100755 index 9a5366350e..0000000000 --- a/hack/run-managed-policy-e2e.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -umask 077 - -if [[ ${EUID} -ne 0 ]]; then - echo "managed-policy fixture must run as root" >&2 - exit 1 -fi -if [[ ${DEVSY_REQUIRE_MSB_MANAGED_POLICY_FIXTURE:-} != true ]]; then - echo "managed-policy E2E wrapper requires the CI expectation marker" >&2 - exit 1 -fi -if [[ -z ${RUNNER_TEMP:-} || ! -d ${RUNNER_TEMP} ]]; then - echo "RUNNER_TEMP must name an existing directory" >&2 - exit 1 -fi -if [[ ${1:-} != ./e2e.test ]]; then - echo "expected the already-built ./e2e.test binary as the first argument" >&2 - exit 1 -fi - -fixture_root=$(mktemp -d "${RUNNER_TEMP%/}/devsy-managed-policy.XXXXXX") -chmod 700 "$fixture_root" -mkdir -m 700 "$fixture_root/upper" "$fixture_root/work" -for directory in "$fixture_root" "$fixture_root/upper" "$fixture_root/work"; do - if [[ $(stat -c '%u:%g:%a' "$directory") != "0:0:700" ]]; then - echo "managed-policy fixture directory has unexpected ownership or mode: $directory" >&2 - exit 1 - fi -done -namespace_id_file="$fixture_root/mount-namespace-id" -parent_mount_namespace=$(readlink /proc/self/ns/mnt) -export RUNNER_TEMP fixture_root namespace_id_file parent_mount_namespace - -# Deliberately retain the small private scratch under RUNNER_TEMP for runner -# job teardown. Do not remove overlay upper/work after an abnormal namespace -# exit: an unobserved namespace reference could still use them. -unshare --mount --propagation private -- /bin/bash -s -- "$fixture_root" "$namespace_id_file" "$parent_mount_namespace" "$@" <<'NAMESPACE' -set -euo pipefail -umask 077 -fixture_root=$1 -namespace_id_file=$2 -parent_mount_namespace=$3 -shift 3 - -if [[ ${EUID} -ne 0 ]]; then - echo "managed-policy namespace must run as root" >&2 - exit 1 -fi -current_mount_namespace=$(readlink /proc/self/ns/mnt) -if [[ $current_mount_namespace == "$parent_mount_namespace" ]]; then - echo "mount namespace did not change" >&2 - exit 1 -fi -printf '%s\n' "$current_mount_namespace" > "$namespace_id_file" - -# Overlay /etc before creating any managed-policy path so no host file changes. -mount -t overlay overlay -o "lowerdir=/etc,upperdir=$fixture_root/upper,workdir=$fixture_root/work" /etc -if ! awk -v upper="$fixture_root/upper" -v work="$fixture_root/work" ' - $5 == "/etc" { - separator = 0 - for (i = 1; i <= NF; i++) if ($i == "-") separator = i - if (separator && $(separator + 1) == "overlay") { - count = split($(separator + 3), options, ",") - for (i = 1; i <= count; i++) { - if (options[i] == "upperdir=" upper) found_upper = 1 - if (options[i] == "workdir=" work) found_work = 1 - } - if (found_upper && found_work) found = 1 - } - } - END { exit !found } -' /proc/self/mountinfo; then - echo "private /etc overlay does not use this fixture's owned upper and work directories" >&2 - exit 1 -fi -if [[ -e /etc/microsandbox || -L /etc/microsandbox ]]; then - echo "managed-policy fixture requires /etc/microsandbox to be absent in the overlay lower layer" >&2 - exit 1 -fi -install -d -o 0 -g 0 -m 755 /etc/microsandbox -policy_tmp=/etc/microsandbox/.managed.json.initial.$$ -printf '%s\n' '{"version":1,"overrides":{}}' > "$policy_tmp" -chown 0:0 "$policy_tmp" -chmod 600 "$policy_tmp" -mv -T -- "$policy_tmp" /etc/microsandbox/managed.json - -export DEVSY_REQUIRE_MSB_MANAGED_POLICY_FIXTURE=true -export DEVSY_TEST_MSB_MANAGED_POLICY=/etc/microsandbox/managed.json -export DEVSY_TEST_MSB_FIXTURE_ROOT="$fixture_root" -export DEVSY_TEST_MSB_PARENT_MNT_NS="$parent_mount_namespace" -export DEVSY_TEST_MSB_EXPECTED_MNT_NS="$current_mount_namespace" -export DEVSY_TEST_MSB_EXPECTED_UPPER="$fixture_root/upper" -export DEVSY_TEST_MSB_EXPECTED_WORK="$fixture_root/work" -exec "$@" -NAMESPACE From 326709e5db5efc73fbf556d04d63262475ec17f9 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Sun, 18 Oct 2026 13:09:45 -0600 Subject: [PATCH 2/2] refactor(ci): compress managed-policy Go helpers --- hack/microsandbox/managed_policy_linux.go | 333 ++++++------- hack/microsandbox/managed_policy_other.go | 15 +- hack/microsandbox/managed_policy_test.go | 579 +++++++++------------- 3 files changed, 378 insertions(+), 549 deletions(-) diff --git a/hack/microsandbox/managed_policy_linux.go b/hack/microsandbox/managed_policy_linux.go index 0df7280e64..f7d51081ed 100644 --- a/hack/microsandbox/managed_policy_linux.go +++ b/hack/microsandbox/managed_policy_linux.go @@ -16,51 +16,35 @@ import ( ) const ( - managedPolicyMarker = "DEVSY_REQUIRE_MSB_MANAGED_POLICY_FIXTURE" - managedPolicyTestBinary = "./e2e.test" - managedPolicyEtcRoot = "/etc" - managedPolicyDirName = "microsandbox" - managedPolicyFileName = "managed.json" - managedPolicyFixturePfx = "devsy-managed-policy." - managedPolicyNamespaceID = "mount-namespace-id" - managedPolicyDocument = "{\"version\":1,\"overrides\":{}}\n" + managedPolicyMarker = "DEVSY_REQUIRE_MSB_MANAGED_POLICY_FIXTURE" + managedPolicyTestBinary = "./e2e.test" + managedPolicyDocument = "{\"version\":1,\"overrides\":{}}\n" + managedPolicyIDFile = "mount-namespace-id" ) -// managedPolicyOwner is the uid/gid required for fixture directories and the policy file. -type managedPolicyOwner struct { - uid uint32 - gid uint32 -} - -// managedPolicyRuntime isolates every privileged side effect so the invariants -// can be exercised without root or a real mount namespace. +// managedPolicyRuntime isolates privileged side effects so tests can run the invariants unprivileged. type managedPolicyRuntime struct { euid int - owner managedPolicyOwner + uid, gid uint32 etcRoot string mountNamespace func() (string, error) mountOverlay func(target, options string) error readMountInfo func() (string, error) - lookPath func(name string) (string, error) + lookPath func(string) (string, error) executable func() (string, error) replace func(path string, argv, env []string) error } -// managedPolicyNamespace is the state handed from the outer wrapper to the -// unshared namespace stage. +// managedPolicyNamespace is the namespace state handed to the test command. type managedPolicyNamespace struct { - root string - idFile string - parent string - current string - command []string + root, parent, current string } +// productionManagedPolicyRuntime leaves uid and gid zero, so fixture and policy files are root-owned. func productionManagedPolicyRuntime() managedPolicyRuntime { return managedPolicyRuntime{ euid: os.Geteuid(), - owner: managedPolicyOwner{}, - etcRoot: managedPolicyEtcRoot, + etcRoot: "/etc", mountNamespace: func() (string, error) { return os.Readlink("/proc/self/ns/mnt") }, mountOverlay: func(target, options string) error { return syscall.Mount("overlay", target, "overlay", 0, options) @@ -75,36 +59,35 @@ func productionManagedPolicyRuntime() managedPolicyRuntime { } } -// managedPolicyE2ECLI runs stage one: validate inputs, create the fixture, and -// re-exec this binary under a private mount namespace. It only returns on error. -func managedPolicyE2ECLI(args []string, stderr io.Writer) int { - syscall.Umask(0o077) - return managedPolicyExit(stderr, runManagedPolicyE2E(productionManagedPolicyRuntime(), args)) -} - -// managedPolicyNamespaceCLI runs stage two inside the unshared mount namespace. -// It is invoked only by managedPolicyE2ECLI. -func managedPolicyNamespaceCLI(args []string, stderr io.Writer) int { +// managedPolicyExit runs one stage under a private umask; a successful stage never returns. +func managedPolicyExit( + stderr io.Writer, + stage func(managedPolicyRuntime, []string) error, + args []string, +) int { syscall.Umask(0o077) - return managedPolicyExit( - stderr, - runManagedPolicyNamespace(productionManagedPolicyRuntime(), args), - ) -} - -func managedPolicyExit(stderr io.Writer, err error) int { - if err != nil { + if err := stage(productionManagedPolicyRuntime(), args); err != nil { _, _ = fmt.Fprintln(stderr, err) return 1 } return 0 } +func managedPolicyE2ECLI(args []string, stderr io.Writer) int { + return managedPolicyExit(stderr, runManagedPolicyE2E, args) +} + +func managedPolicyNamespaceCLI(args []string, stderr io.Writer) int { + return managedPolicyExit(stderr, runManagedPolicyNamespace, args) +} + +// runManagedPolicyE2E creates the fixture and re-execs this binary inside a private mount namespace. func runManagedPolicyE2E(rt managedPolicyRuntime, args []string) error { - if err := checkManagedPolicyInputs(rt, args); err != nil { + runnerTemp := os.Getenv("RUNNER_TEMP") + if err := checkManagedPolicyInputs(rt, runnerTemp, args); err != nil { return err } - root, err := createManagedPolicyFixture(os.Getenv("RUNNER_TEMP"), rt.owner) + root, err := createManagedPolicyFixture(runnerTemp, rt.uid, rt.gid) if err != nil { return err } @@ -120,134 +103,107 @@ func runManagedPolicyE2E(rt managedPolicyRuntime, args []string) error { if err != nil { return fmt.Errorf("find unshare: %w", err) } - argv := []string{ - "unshare", - "--mount", - "--propagation", - "private", - "--", - self, - managedPolicyNamespaceCommand, - root, - filepath.Join(root, managedPolicyNamespaceID), - parent, - } - argv = append(argv, args...) + argv := append([]string{ + "unshare", "--mount", "--propagation", "private", "--", self, + managedPolicyNamespaceCommand, root, filepath.Join(root, managedPolicyIDFile), parent, + }, args...) if err := rt.replace(unshare, argv, os.Environ()); err != nil { return fmt.Errorf("exec unshare: %w", err) } return nil } -func checkManagedPolicyInputs(rt managedPolicyRuntime, args []string) error { - if rt.euid != 0 { +func checkManagedPolicyInputs(rt managedPolicyRuntime, runnerTemp string, args []string) error { + info, statErr := os.Stat(runnerTemp) + switch { + case rt.euid != 0: return errors.New("managed-policy fixture must run as root") - } - if os.Getenv(managedPolicyMarker) != "true" { + case os.Getenv(managedPolicyMarker) != "true": return errors.New("managed-policy E2E wrapper requires the CI expectation marker") - } - if !isExistingDirectory(os.Getenv("RUNNER_TEMP")) { + case statErr != nil || !info.IsDir(): return errors.New("RUNNER_TEMP must name an existing directory") - } - if len(args) == 0 || args[0] != managedPolicyTestBinary { + case len(args) == 0 || args[0] != managedPolicyTestBinary: return errors.New("expected the already-built ./e2e.test binary as the first argument") } return nil } -func isExistingDirectory(path string) bool { - if path == "" { - return false - } - info, err := os.Stat(path) - return err == nil && info.IsDir() -} - -// createManagedPolicyFixture creates /devsy-managed-policy.* with -// upper and work subdirectories, all owned by owner with mode 0700. -func createManagedPolicyFixture(runnerTemp string, owner managedPolicyOwner) (string, error) { - parent := strings.TrimRight(runnerTemp, "/") - if parent == "" { - parent = "/" - } - root, err := os.MkdirTemp(parent, managedPolicyFixturePfx) +// createManagedPolicyFixture creates /devsy-managed-policy.* with upper and work +// subdirectories. The CLI umask keeps every directory at 0700; ownership is then verified. +func createManagedPolicyFixture(runnerTemp string, uid, gid uint32) (string, error) { + root, err := os.MkdirTemp(filepath.Clean(runnerTemp), "devsy-managed-policy.") if err != nil { return "", fmt.Errorf("create managed-policy fixture: %w", err) } - if err := os.Chmod(root, 0o700); err != nil { - return "", fmt.Errorf("chmod managed-policy fixture: %w", err) - } - upper, work := filepath.Join(root, "upper"), filepath.Join(root, "work") - for _, dir := range []string{upper, work} { + dirs := []string{root, filepath.Join(root, "upper"), filepath.Join(root, "work")} + for _, dir := range dirs[1:] { if err := os.Mkdir(dir, 0o700); err != nil { return "", fmt.Errorf("create managed-policy fixture directory: %w", err) } } - for _, dir := range []string{root, upper, work} { - if err := verifyManagedPolicyDirectory(dir, owner); err != nil { + for _, dir := range dirs { + if err := verifyManagedPolicyDirectory(dir, uid, gid); err != nil { return "", err } } return root, nil } -func verifyManagedPolicyDirectory(path string, owner managedPolicyOwner) error { - info, err := os.Stat(path) +// verifyManagedPolicyDirectory requires dir to be owned by uid:gid with mode 0700. +func verifyManagedPolicyDirectory(dir string, uid, gid uint32) error { + info, err := os.Stat(dir) if err != nil { - return fmt.Errorf("inspect managed-policy fixture directory %s: %w", path, err) + return fmt.Errorf("inspect managed-policy fixture directory %s: %w", dir, err) } stat, ok := info.Sys().(*syscall.Stat_t) - if !ok || stat.Uid != owner.uid || stat.Gid != owner.gid || stat.Mode&0o7777 != 0o700 { + if !ok || stat.Uid != uid || stat.Gid != gid || stat.Mode&0o7777 != 0o700 { return fmt.Errorf( "managed-policy fixture directory has unexpected ownership or mode: %s", - path, + dir, ) } return nil } +// runManagedPolicyNamespace runs inside the unshared namespace: it overlays /etc, installs the +// initial policy, and execs the test command. func runManagedPolicyNamespace(rt managedPolicyRuntime, args []string) error { - if rt.euid != 0 { - return errors.New("managed-policy namespace must run as root") - } ns, err := enterManagedPolicyNamespace(rt, args) if err != nil { return err } - upper, work := filepath.Join(ns.root, "upper"), filepath.Join(ns.root, "work") - options := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", rt.etcRoot, upper, work) - if err := rt.mountOverlay(rt.etcRoot, options); err != nil { - return fmt.Errorf("mount overlay on %s: %w", rt.etcRoot, err) - } - if err := verifyOverlayMount(rt, upper, work); err != nil { + if err := mountManagedPolicyOverlay(rt, ns.root); err != nil { return err } - if err := ensureManagedPolicyDirAbsent(rt.etcRoot); err != nil { - return err - } - policy, err := writeInitialManagedPolicy(rt.etcRoot, rt.owner) + policy, err := installManagedPolicy(rt) if err != nil { return err } - env := managedPolicyChildEnvironment(os.Environ(), ns, policy) - if err := rt.replace(ns.command[0], ns.command, env); err != nil { - return fmt.Errorf("exec %s: %w", ns.command[0], err) + command := args[3:] + if err := rt.replace( + command[0], + command, + managedPolicyChildEnvironment(os.Environ(), ns, policy), + ); err != nil { + return fmt.Errorf("exec %s: %w", command[0], err) } return nil } -// enterManagedPolicyNamespace validates the stage-two arguments, confirms the -// mount namespace changed, and records its id for the test process. +// enterManagedPolicyNamespace checks that the mount namespace changed and records its id. func enterManagedPolicyNamespace( rt managedPolicyRuntime, args []string, ) (managedPolicyNamespace, error) { + if rt.euid != 0 { + return managedPolicyNamespace{}, errors.New("managed-policy namespace must run as root") + } if len(args) < 4 { return managedPolicyNamespace{}, errors.New( "managed-policy-namespace requires ", ) } - ns := managedPolicyNamespace{root: args[0], idFile: args[1], parent: args[2], command: args[3:]} + ns := managedPolicyNamespace{root: args[0], parent: args[2]} current, err := rt.mountNamespace() if err != nil { return ns, fmt.Errorf("read mount namespace: %w", err) @@ -256,13 +212,20 @@ func enterManagedPolicyNamespace( return ns, errors.New("mount namespace did not change") } ns.current = current - if err := os.WriteFile(ns.idFile, []byte(current+"\n"), 0o600); err != nil { + if err := os.WriteFile(args[1], []byte(current+"\n"), 0o600); err != nil { return ns, fmt.Errorf("write mount namespace id: %w", err) } return ns, nil } -func verifyOverlayMount(rt managedPolicyRuntime, upper, work string) error { +// mountManagedPolicyOverlay overlays etc before any managed-policy path is created, so no host +// file changes, and verifies the overlay uses this fixture's upper and work directories. +func mountManagedPolicyOverlay(rt managedPolicyRuntime, root string) error { + upper, work := filepath.Join(root, "upper"), filepath.Join(root, "work") + options := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", rt.etcRoot, upper, work) + if err := rt.mountOverlay(rt.etcRoot, options); err != nil { + return fmt.Errorf("mount overlay on %s: %w", rt.etcRoot, err) + } mountInfo, err := rt.readMountInfo() if err != nil { return fmt.Errorf("read mountinfo: %w", err) @@ -275,89 +238,55 @@ func verifyOverlayMount(rt managedPolicyRuntime, upper, work string) error { return nil } -// hasOverlayMount reports whether a mountinfo line shows an overlay at mountPoint -// whose super options name exactly the upper and work directories. -func hasOverlayMount(mountInfo, mountPoint, upper, work string) bool { - for line := range strings.SplitSeq(mountInfo, "\n") { - if overlayLineMatches(strings.Fields(line), mountPoint, upper, work) { - return true - } - } - return false -} - -// overlayLineMatches reports whether one mountinfo line's overlay options name -// the given upper and work directories. -func overlayLineMatches(fields []string, mountPoint, upper, work string) bool { - if len(fields) < 5 || fields[4] != mountPoint { - return false - } - sep := lastSeparatorIndex(fields) - if sep < 0 || sep+3 >= len(fields) || fields[sep+1] != "overlay" { - return false - } - options := strings.Split(fields[sep+3], ",") - return slices.Contains(options, "upperdir="+upper) && slices.Contains(options, "workdir="+work) -} - -func lastSeparatorIndex(fields []string) int { - sep := -1 - for i, field := range fields { - if field == "-" { - sep = i - } +// installManagedPolicy creates /microsandbox/managed.json holding the empty version-1 policy. +func installManagedPolicy(rt managedPolicyRuntime) (string, error) { + dir := filepath.Join(rt.etcRoot, "microsandbox") + if err := createManagedPolicyDir(rt, dir); err != nil { + return "", err } - return sep + return writeManagedPolicy(rt, dir) } -func ensureManagedPolicyDirAbsent(etcRoot string) error { - _, err := os.Lstat(filepath.Join(etcRoot, managedPolicyDirName)) +// createManagedPolicyDir requires the directory to be absent from the lower layer, then creates it 0755. +func createManagedPolicyDir(rt managedPolicyRuntime, dir string) error { + _, err := os.Lstat(dir) if err == nil { return errors.New( "managed-policy fixture requires /etc/microsandbox to be absent in the overlay lower layer", ) } - if errors.Is(err, fs.ErrNotExist) { - return nil + if !errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("inspect %s: %w", dir, err) } - return fmt.Errorf("inspect /etc/microsandbox: %w", err) -} - -// writeInitialManagedPolicy creates /microsandbox/managed.json with the -// empty version-1 policy, written via an exclusive temp file and renamed into place. -func writeInitialManagedPolicy(etcRoot string, owner managedPolicyOwner) (string, error) { - dir := filepath.Join(etcRoot, managedPolicyDirName) if err := os.Mkdir(dir, 0o755); err != nil { - return "", fmt.Errorf("create %s: %w", dir, err) - } - if err := setManagedPolicyOwner(dir, owner, 0o755); err != nil { - return "", err + return fmt.Errorf("create %s: %w", dir, err) } + return setOwnerMode(dir, rt.uid, rt.gid, 0o755) +} + +// writeManagedPolicy writes the policy to an exclusive temp file and renames it into place. +func writeManagedPolicy(rt managedPolicyRuntime, dir string) (string, error) { tmp := filepath.Join(dir, fmt.Sprintf(".managed.json.initial.%d", os.Getpid())) - if err := createManagedPolicyTemp(tmp); err != nil { + file, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) + if err != nil { return "", fmt.Errorf("write initial managed policy: %w", err) } - if err := setManagedPolicyOwner(tmp, owner, 0o600); err != nil { + _, writeErr := file.WriteString(managedPolicyDocument) + if err := errors.Join(writeErr, file.Close()); err != nil { + return "", fmt.Errorf("write initial managed policy: %w", err) + } + if err := setOwnerMode(tmp, rt.uid, rt.gid, 0o600); err != nil { return "", err } - policy := filepath.Join(dir, managedPolicyFileName) + policy := filepath.Join(dir, "managed.json") if err := os.Rename(tmp, policy); err != nil { return "", fmt.Errorf("install managed policy: %w", err) } return policy, nil } -func createManagedPolicyTemp(path string) error { - file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) - if err != nil { - return err - } - _, writeErr := file.WriteString(managedPolicyDocument) - return errors.Join(writeErr, file.Close()) -} - -func setManagedPolicyOwner(path string, owner managedPolicyOwner, mode os.FileMode) error { - if err := os.Chown(path, int(owner.uid), int(owner.gid)); err != nil { +func setOwnerMode(path string, uid, gid uint32, mode os.FileMode) error { + if err := os.Chown(path, int(uid), int(gid)); err != nil { return fmt.Errorf("chown %s: %w", path, err) } if err := os.Chmod(path, mode); err != nil { @@ -366,8 +295,33 @@ func setManagedPolicyOwner(path string, owner managedPolicyOwner, mode os.FileMo return nil } -// managedPolicyChildEnvironment returns environ with every managed-policy key -// replaced by the values the e2e test binary's consumer expects. +// hasOverlayMount reports whether one mountinfo line shows an overlay at mountPoint whose super +// options name exactly the upper and work directories. Both must share a line. +func hasOverlayMount(mountInfo, mountPoint, upper, work string) bool { + for line := range strings.SplitSeq(mountInfo, "\n") { + options := overlaySuperOptions(line, mountPoint) + if options != nil && slices.Contains(options, "upperdir="+upper) && + slices.Contains(options, "workdir="+work) { + return true + } + } + return false +} + +// overlaySuperOptions returns the super options of a mountinfo line that is an overlay at mountPoint, +// or nil. The single "-" separator splits optional fields from the fstype and source fields. +func overlaySuperOptions(line, mountPoint string) []string { + fields := strings.Fields(line) + sep := slices.Index(fields, "-") + if len(fields) < 5 || fields[4] != mountPoint || sep < 0 || sep+3 >= len(fields) || + fields[sep+1] != "overlay" { + return nil + } + return strings.Split(fields[sep+3], ",") +} + +// managedPolicyChildEnvironment replaces every managed-policy key in environ with the values +// the e2e test binary expects. func managedPolicyChildEnvironment( environ []string, ns managedPolicyNamespace, @@ -382,17 +336,12 @@ func managedPolicyChildEnvironment( "DEVSY_TEST_MSB_EXPECTED_UPPER=" + filepath.Join(ns.root, "upper"), "DEVSY_TEST_MSB_EXPECTED_WORK=" + filepath.Join(ns.root, "work"), } - managed := make(map[string]bool, len(overrides)) - for _, assignment := range overrides { - key, _, _ := strings.Cut(assignment, "=") - managed[key] = true - } - env := make([]string, 0, len(environ)+len(overrides)) - for _, entry := range environ { + kept := slices.DeleteFunc(slices.Clone(environ), func(entry string) bool { key, _, _ := strings.Cut(entry, "=") - if !managed[key] { - env = append(env, entry) - } - } - return append(env, overrides...) + return slices.ContainsFunc( + overrides, + func(o string) bool { return strings.HasPrefix(o, key+"=") }, + ) + }) + return append(kept, overrides...) } diff --git a/hack/microsandbox/managed_policy_other.go b/hack/microsandbox/managed_policy_other.go index 6e7a09c87f..d1a43847f6 100644 --- a/hack/microsandbox/managed_policy_other.go +++ b/hack/microsandbox/managed_policy_other.go @@ -8,17 +8,8 @@ import ( "runtime" ) -// managedPolicyE2ECLI rejects the wrapper off Linux, which needs mount namespaces and overlayfs. +// managedPolicyE2ECLI rejects managed-policy-e2e off Linux, which needs mount namespaces and overlayfs. func managedPolicyE2ECLI(_ []string, stderr io.Writer) int { - return managedPolicyUnsupported(stderr) -} - -// managedPolicyNamespaceCLI rejects the internal namespace stage off Linux. -func managedPolicyNamespaceCLI(_ []string, stderr io.Writer) int { - return managedPolicyUnsupported(stderr) -} - -func managedPolicyUnsupported(stderr io.Writer) int { _, _ = fmt.Fprintf( stderr, "managed-policy-e2e is unsupported on %s: requires Linux mount namespaces and overlayfs\n", @@ -26,3 +17,7 @@ func managedPolicyUnsupported(stderr io.Writer) int { ) return 2 } + +func managedPolicyNamespaceCLI(args []string, stderr io.Writer) int { + return managedPolicyE2ECLI(args, stderr) +} diff --git a/hack/microsandbox/managed_policy_test.go b/hack/microsandbox/managed_policy_test.go index 983ac1c1af..00b1df66b9 100644 --- a/hack/microsandbox/managed_policy_test.go +++ b/hack/microsandbox/managed_policy_test.go @@ -12,65 +12,56 @@ import ( ) const ( - managedPolicyTrue = "true" - managedPolicyParentNS = "mnt:[parent]" - managedPolicyChildNS = "mnt:[child]" - managedPolicyTestArg = "-test.v" - managedPolicyEnvPath = "PATH=/bin" - managedPolicyNotDirMsg = "RUNNER_TEMP must name an existing directory" - managedPolicyBinaryMsg = "expected the already-built ./e2e.test binary as the first argument" - managedPolicyAbsentMsg = "managed-policy fixture requires /etc/microsandbox to be absent in the overlay lower layer" + managedPolicyParentNS = "mnt:[parent]" + managedPolicyChildNS = "mnt:[child]" + managedPolicyPathEnv = "PATH=/bin" + managedPolicyTestArg = "-test.v" + managedPolicyEnabled = "true" ) +// managedPolicyFake records privileged calls instead of performing them. type managedPolicyFake struct { - current string - overlayTarget string - overlayOptions string - mountInfo string - replaced bool - path string - argv []string - env []string + namespace string + mountInfo string + options string + exec string + argv []string + env []string } -// newManagedPolicyFake returns a runtime whose privileged calls are recorded -// instead of performed. Ownership is the test process's own uid/gid so the -// fixture checks pass without root. -func newManagedPolicyFake(euid int, etcRoot string) (managedPolicyRuntime, *managedPolicyFake) { - fake := &managedPolicyFake{current: managedPolicyChildNS} +// newManagedPolicyFake returns a root-like runtime whose ownership checks match the test user. +func newManagedPolicyFake(etc string) (managedPolicyRuntime, *managedPolicyFake) { + fake := &managedPolicyFake{namespace: managedPolicyParentNS} rt := managedPolicyRuntime{ - euid: euid, - owner: managedPolicyOwner{uid: uint32(os.Getuid()), gid: uint32(os.Getgid())}, - etcRoot: etcRoot, - mountNamespace: func() (string, error) { - return fake.current, nil - }, - mountOverlay: func(target, options string) error { - fake.overlayTarget, fake.overlayOptions = target, options + euid: 0, + uid: uint32(os.Getuid()), + gid: uint32(os.Getgid()), + etcRoot: etc, + mountNamespace: func() (string, error) { return fake.namespace, nil }, + mountOverlay: func(_, options string) error { + fake.options = options return nil }, - readMountInfo: func() (string, error) { - if fake.mountInfo != "" { - return fake.mountInfo, nil - } - return fmt.Sprintf( - "36 25 0:32 / %s rw - overlay overlay %s", - fake.overlayTarget, - fake.overlayOptions, - ), nil - }, - lookPath: func(string) (string, error) { return "/usr/bin/unshare", nil }, - executable: func() (string, error) { - return "/abs/devsy", nil - }, + readMountInfo: func() (string, error) { return fake.mountInfo, nil }, + lookPath: func(name string) (string, error) { return "/usr/bin/" + name, nil }, + executable: func() (string, error) { return "/bin/e2e-wrapper", nil }, replace: func(path string, argv, env []string) error { - fake.replaced, fake.path, fake.argv, fake.env = true, path, argv, env + fake.exec, fake.argv, fake.env = path, argv, env return nil }, } return rt, fake } +func overlayMountInfo(mountPoint, upper, work string) string { + return fmt.Sprintf( + "36 25 0:31 / %s rw,relatime - overlay overlay rw,lowerdir=/etc,upperdir=%s,workdir=%s", + mountPoint, + upper, + work, + ) +} + func requireManagedPolicyMode(t *testing.T, path string, want os.FileMode) { t.Helper() info, err := os.Stat(path) @@ -78,389 +69,283 @@ func requireManagedPolicyMode(t *testing.T, path string, want os.FileMode) { t.Fatal(err) } if got := info.Mode().Perm(); got != want { - t.Errorf("%s mode = %o, want %o", path, got, want) + t.Fatalf("%s mode = %o, want %o", path, got, want) } } -type managedPolicyInputCase struct { - name string - euid int - marker string - runnerTemp string - args []string - want string +// requireUnshareInvocation checks the argv that stage 1 hands to unshare. +func requireUnshareInvocation(t *testing.T, argv []string, runnerTemp string, args []string) { + t.Helper() + // argv: 0 unshare, 1-4 flags, 5 self, 6 namespace cmd, 7 root, 8 id file, 9 parent, 10+ args. + if len(argv) < 10 || argv[6] != managedPolicyNamespaceCommand || + argv[9] != managedPolicyParentNS { + t.Fatalf("argv = %q", argv) + } + if !slices.Equal(argv[10:], args) { + t.Fatalf("forwarded args = %q, want %q", argv[10:], args) + } + requireFixtureRoot(t, argv[7], argv[8], runnerTemp) } -func managedPolicyInputCases(validTemp string) []managedPolicyInputCase { - binary := []string{managedPolicyTestBinary} - return []managedPolicyInputCase{ - { - name: "non-root", euid: 1000, marker: managedPolicyTrue, runnerTemp: validTemp, - args: binary, want: "managed-policy fixture must run as root", - }, - { - name: "missing marker", euid: 0, marker: "", runnerTemp: validTemp, - args: binary, want: "managed-policy E2E wrapper requires the CI expectation marker", - }, +// requireFixtureRoot checks the fixture lies under RUNNER_TEMP, the id file is inside it, +// and its directories are 0700. +func requireFixtureRoot(t *testing.T, root, idFile, runnerTemp string) { + t.Helper() + if filepath.Dir(root) != runnerTemp || + !strings.HasPrefix(filepath.Base(root), "devsy-managed-policy.") { + t.Fatalf("fixture root %q not under RUNNER_TEMP", root) + } + if idFile != filepath.Join(root, managedPolicyIDFile) { + t.Fatalf("id file = %q", idFile) + } + for _, dir := range []string{root, filepath.Join(root, "upper"), filepath.Join(root, "work")} { + requireManagedPolicyMode(t, dir, 0o700) + } +} + +// requireNamespaceID checks the id file written by stage 2 holds the child namespace. +func requireNamespaceID(t *testing.T, idFile, want string) { + t.Helper() + id, err := os.ReadFile(idFile) + if err != nil || string(id) != want+"\n" { + t.Fatalf("id file = %q, %v", id, err) + } + requireManagedPolicyMode(t, idFile, 0o600) +} + +// requireInstalledPolicy checks the initial policy file and its directory. +func requireInstalledPolicy(t *testing.T, policy string) { + t.Helper() + data, err := os.ReadFile(policy) + if err != nil || string(data) != managedPolicyDocument { + t.Fatalf("policy = %q, %v", data, err) + } + requireManagedPolicyMode(t, policy, 0o600) + requireManagedPolicyMode(t, filepath.Dir(policy), 0o755) +} + +func TestManagedPolicyE2EInputInvariants(t *testing.T) { + valid := t.TempDir() + good := []string{managedPolicyTestBinary} + cases := []struct { + name, marker, runnerTemp, want string + euid int + args []string + }{ { - name: "empty runner temp", euid: 0, marker: managedPolicyTrue, runnerTemp: "", - args: binary, want: managedPolicyNotDirMsg, + "non-root", managedPolicyEnabled, valid, + "managed-policy fixture must run as root", 1000, good, }, { - name: "runner temp absent", - euid: 0, - marker: managedPolicyTrue, - runnerTemp: filepath.Join( - validTemp, - "absent", - ), - args: binary, - want: managedPolicyNotDirMsg, + "missing marker", "", valid, + "managed-policy E2E wrapper requires the CI expectation marker", 0, good, }, { - name: "wrong binary", euid: 0, marker: managedPolicyTrue, runnerTemp: validTemp, - args: []string{"./other"}, want: managedPolicyBinaryMsg, + "missing runner temp", managedPolicyEnabled, filepath.Join(valid, "absent"), + "RUNNER_TEMP must name an existing directory", 0, good, }, { - name: "no arguments", euid: 0, marker: managedPolicyTrue, runnerTemp: validTemp, - want: managedPolicyBinaryMsg, + "wrong binary", managedPolicyEnabled, valid, + "expected the already-built ./e2e.test binary as the first argument", 0, + []string{"./other.test"}, }, } -} - -func TestManagedPolicyE2EInputInvariants(t *testing.T) { - for _, tc := range managedPolicyInputCases(t.TempDir()) { + for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Setenv(managedPolicyMarker, tc.marker) t.Setenv("RUNNER_TEMP", tc.runnerTemp) - rt, fake := newManagedPolicyFake(tc.euid, t.TempDir()) + rt, fake := newManagedPolicyFake(t.TempDir()) + rt.euid = tc.euid err := runManagedPolicyE2E(rt, tc.args) if err == nil || err.Error() != tc.want { - t.Fatalf("runManagedPolicyE2E() error = %v, want %q", err, tc.want) + t.Fatalf("error = %v, want %q", err, tc.want) } - if fake.replaced { - t.Fatal("re-exec happened despite invalid input") + if fake.exec != "" { + t.Fatal("re-exec must not happen on invalid input") } }) } } -func TestManagedPolicyE2EReexecsWithFixtureUnderRunnerTemp(t *testing.T) { +func TestManagedPolicyE2EReexecsInFixtureUnderRunnerTemp(t *testing.T) { runnerTemp := t.TempDir() - t.Setenv(managedPolicyMarker, managedPolicyTrue) + t.Setenv(managedPolicyMarker, managedPolicyEnabled) t.Setenv("RUNNER_TEMP", runnerTemp) - rt, fake := newManagedPolicyFake(0, t.TempDir()) - fake.current = managedPolicyParentNS - - if err := runManagedPolicyE2E( - rt, - []string{managedPolicyTestBinary, managedPolicyTestArg}, - ); err != nil { + rt, fake := newManagedPolicyFake(t.TempDir()) + args := []string{managedPolicyTestBinary, managedPolicyTestArg} + if err := runManagedPolicyE2E(rt, args); err != nil { t.Fatal(err) } - if !fake.replaced { - t.Fatal("runManagedPolicyE2E() did not re-exec") - } - if len(fake.argv) < 8 { - t.Fatalf("argv = %#v, too short", fake.argv) - } - root := fake.argv[7] - assertManagedPolicyFixtureRoot(t, root, runnerTemp) - want := []string{ - "unshare", "--mount", "--propagation", "private", "--", - "/abs/devsy", "managed-policy-namespace", root, filepath.Join(root, "mount-namespace-id"), - managedPolicyParentNS, managedPolicyTestBinary, managedPolicyTestArg, - } - if fake.path != "/usr/bin/unshare" || !slices.Equal(fake.argv, want) { - t.Fatalf("exec path = %q argv = %#v, want /usr/bin/unshare %#v", fake.path, fake.argv, want) - } -} - -func assertManagedPolicyFixtureRoot(t *testing.T, root, runnerTemp string) { - t.Helper() - if filepath.Dir(root) != runnerTemp || - !strings.HasPrefix(filepath.Base(root), "devsy-managed-policy.") { - t.Fatalf( - "fixture root %q is not a devsy-managed-policy directory under RUNNER_TEMP %q", - root, - runnerTemp, - ) - } - for _, dir := range []string{root, filepath.Join(root, "upper"), filepath.Join(root, "work")} { - requireManagedPolicyMode(t, dir, 0o700) + if fake.exec != "/usr/bin/unshare" { + t.Fatalf("exec = %q, want /usr/bin/unshare", fake.exec) } + requireUnshareInvocation(t, fake.argv, runnerTemp, args) } func TestCreateManagedPolicyFixtureRejectsUnexpectedOwner(t *testing.T) { - runnerTemp := t.TempDir() - wrongOwner := managedPolicyOwner{uid: uint32(os.Getuid()) + 1, gid: uint32(os.Getgid())} - _, err := createManagedPolicyFixture(runnerTemp, wrongOwner) - if err == nil || - !strings.HasPrefix( - err.Error(), - "managed-policy fixture directory has unexpected ownership or mode: ", - ) { - t.Fatalf("createManagedPolicyFixture() error = %v, want ownership failure", err) + _, err := createManagedPolicyFixture(t.TempDir(), uint32(os.Getuid())+1, uint32(os.Getgid())) + if err == nil || !strings.Contains(err.Error(), "unexpected ownership or mode") { + t.Fatalf("error = %v", err) } } -type managedPolicyNamespaceCase struct { - name string - euid int - args []string - setup func(t *testing.T, etc string, fake *managedPolicyFake) - want string -} - -func mkExistingPolicyDir(t *testing.T, etc string, _ *managedPolicyFake) { - t.Helper() - if err := os.Mkdir(filepath.Join(etc, "microsandbox"), 0o755); err != nil { +func TestManagedPolicyNamespaceInstallsPolicyAndExecs(t *testing.T) { + root, etc := t.TempDir(), t.TempDir() + rt, fake := newManagedPolicyFake(etc) + fake.namespace = managedPolicyChildNS + upper, work := filepath.Join(root, "upper"), filepath.Join(root, "work") + fake.mountInfo = overlayMountInfo(etc, upper, work) + idFile := filepath.Join(root, managedPolicyIDFile) + command := []string{managedPolicyTestBinary, managedPolicyTestArg} + args := append([]string{root, idFile, managedPolicyParentNS}, command...) + if err := runManagedPolicyNamespace(rt, args); err != nil { t.Fatal(err) } -} - -func linkDanglingPolicy(t *testing.T, etc string, _ *managedPolicyFake) { - t.Helper() - if err := os.Symlink("/nonexistent/target", filepath.Join(etc, "microsandbox")); err != nil { - t.Fatal(err) + requireNamespaceID(t, idFile, managedPolicyChildNS) + policy := filepath.Join(etc, "microsandbox", "managed.json") + requireInstalledPolicy(t, policy) + if !strings.Contains(fake.options, "upperdir="+upper) || + !strings.Contains(fake.options, "workdir="+work) { + t.Fatalf("overlay options = %q", fake.options) + } + if fake.exec != managedPolicyTestBinary || !slices.Equal(fake.argv, command) { + t.Fatalf("exec = %q argv = %q", fake.exec, fake.argv) + } + if !slices.Contains(fake.env, "DEVSY_TEST_MSB_MANAGED_POLICY="+policy) { + t.Fatal("child env missing policy path") } } -func setParentNamespace(_ *testing.T, _ string, fake *managedPolicyFake) { - fake.current = managedPolicyParentNS -} - -func setForeignOverlay(_ *testing.T, _ string, fake *managedPolicyFake) { - fake.mountInfo = "36 25 0:32 / /etc rw - overlay overlay " + - "lowerdir=/etc,upperdir=/elsewhere/upper,workdir=/elsewhere/work" -} - -func managedPolicyNamespaceCases() []managedPolicyNamespaceCase { - return []managedPolicyNamespaceCase{ - {name: "non-root", euid: 1000, want: "managed-policy namespace must run as root"}, - { - name: "too few arguments", - euid: 0, - args: []string{"root", "id", managedPolicyParentNS}, - want: "managed-policy-namespace requires ", - }, - { - name: "namespace unchanged", - euid: 0, - setup: setParentNamespace, - want: "mount namespace did not change", - }, - { - name: "mountinfo names other directories", - euid: 0, - setup: setForeignOverlay, - want: "private /etc overlay does not use this fixture's owned upper and work directories", - }, - { - name: "existing policy directory", - euid: 0, - setup: mkExistingPolicyDir, - want: managedPolicyAbsentMsg, - }, - { - name: "dangling policy symlink", - euid: 0, - setup: linkDanglingPolicy, - want: managedPolicyAbsentMsg, - }, - } +// namespaceViolation is one stage-2 invariant case; setup prepares the fake runtime and etc tree. +type namespaceViolation struct { + name, want string + args []string + setup func(t *testing.T, etc string, rt *managedPolicyRuntime, fake *managedPolicyFake) } -func TestManagedPolicyNamespaceInvariants(t *testing.T) { - for _, tc := range managedPolicyNamespaceCases() { +func runNamespaceViolations(t *testing.T, cases []namespaceViolation) { + t.Helper() + for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { etc := t.TempDir() - root := t.TempDir() - rt, fake := newManagedPolicyFake(tc.euid, etc) - if tc.setup != nil { - tc.setup(t, etc, fake) - } - args := tc.args - if args == nil { - args = []string{ - root, - filepath.Join(root, "id"), - managedPolicyParentNS, - managedPolicyTestBinary, - } - } - err := runManagedPolicyNamespace(rt, args) + rt, fake := newManagedPolicyFake(etc) + tc.setup(t, etc, &rt, fake) + err := runManagedPolicyNamespace(rt, tc.args) if err == nil || err.Error() != tc.want { - t.Fatalf("runManagedPolicyNamespace() error = %v, want %q", err, tc.want) + t.Fatalf("error = %v, want %q", err, tc.want) } - if fake.replaced { - t.Fatal("namespace stage executed the command despite failing invariant") + if fake.exec != "" { + t.Fatal("re-exec must not happen on invariant violation") } }) } } -func TestManagedPolicyNamespaceInstallsPolicyAndExecsCommand(t *testing.T) { - etc := t.TempDir() +func TestManagedPolicyNamespaceRejectsInvariantViolations(t *testing.T) { root := t.TempDir() - t.Setenv("DEVSY_TEST_MSB_FIXTURE_ROOT", "/stale") - rt, fake := newManagedPolicyFake(0, etc) - idFile := filepath.Join(root, "mount-namespace-id") - + upper, work := filepath.Join(root, "upper"), filepath.Join(root, "work") args := []string{ root, - idFile, + filepath.Join(root, managedPolicyIDFile), managedPolicyParentNS, managedPolicyTestBinary, - managedPolicyTestArg, - } - if err := runManagedPolicyNamespace(rt, args); err != nil { - t.Fatal(err) - } - assertManagedPolicyNamespaceID(t, idFile) - assertManagedPolicyOverlay(t, fake, etc, root) - assertManagedPolicyInstalled(t, etc) - assertManagedPolicyExec(t, fake, etc, root) -} - -func assertManagedPolicyNamespaceID(t *testing.T, idFile string) { - t.Helper() - id, err := os.ReadFile(idFile) - if err != nil || string(id) != managedPolicyChildNS+"\n" { - t.Fatalf("namespace id = %q, err = %v", id, err) - } - requireManagedPolicyMode(t, idFile, 0o600) -} - -func assertManagedPolicyOverlay(t *testing.T, fake *managedPolicyFake, etc, root string) { - t.Helper() - wantOptions := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", - etc, filepath.Join(root, "upper"), filepath.Join(root, "work")) - if fake.overlayTarget != etc || fake.overlayOptions != wantOptions { - t.Fatalf( - "overlay = %q %q, want %q %q", - fake.overlayTarget, - fake.overlayOptions, - etc, - wantOptions, - ) - } -} - -func assertManagedPolicyInstalled(t *testing.T, etc string) { - t.Helper() - policyDir := filepath.Join(etc, "microsandbox") - policy := filepath.Join(policyDir, "managed.json") - data, err := os.ReadFile(policy) - if err != nil || string(data) != managedPolicyDocument { - t.Fatalf("policy = %q, err = %v", data, err) - } - requireManagedPolicyMode(t, policy, 0o600) - requireManagedPolicyMode(t, policyDir, 0o755) - entries, err := os.ReadDir(policyDir) - if err != nil || len(entries) != 1 { - t.Fatalf("policy dir entries = %v, err = %v; want only managed.json", entries, err) - } -} - -func assertManagedPolicyExec(t *testing.T, fake *managedPolicyFake, etc, root string) { - t.Helper() - wantArgv := []string{managedPolicyTestBinary, managedPolicyTestArg} - if !fake.replaced || fake.path != managedPolicyTestBinary || - !slices.Equal(fake.argv, wantArgv) { - t.Fatalf("exec path = %q argv = %#v", fake.path, fake.argv) - } - if slices.Contains(fake.env, "DEVSY_TEST_MSB_FIXTURE_ROOT=/stale") { - t.Fatal("stale DEVSY_TEST_MSB_FIXTURE_ROOT leaked into child environment") - } - policy := filepath.Join(etc, "microsandbox", "managed.json") - for _, want := range []string{ - "DEVSY_REQUIRE_MSB_MANAGED_POLICY_FIXTURE=" + managedPolicyTrue, - "DEVSY_TEST_MSB_MANAGED_POLICY=" + policy, - "DEVSY_TEST_MSB_FIXTURE_ROOT=" + root, - "DEVSY_TEST_MSB_EXPECTED_UPPER=" + filepath.Join(root, "upper"), - } { - if !slices.Contains(fake.env, want) { - t.Errorf("child environment missing %q", want) - } } + noSetup := func(*testing.T, string, *managedPolicyRuntime, *managedPolicyFake) {} + runNamespaceViolations(t, []namespaceViolation{ + { + name: "not root", want: "managed-policy namespace must run as root", args: args, + setup: func(_ *testing.T, _ string, rt *managedPolicyRuntime, _ *managedPolicyFake) { + rt.euid = 1000 + }, + }, + { + name: "too few args", + setup: noSetup, + args: args[:3], + want: "managed-policy-namespace requires ", + }, + { + name: "namespace unchanged", + want: "mount namespace did not change", + args: args, + setup: noSetup, + }, + { + name: "foreign overlay", + args: args, + setup: func(_ *testing.T, etc string, _ *managedPolicyRuntime, fake *managedPolicyFake) { + fake.namespace = managedPolicyChildNS + fake.mountInfo = overlayMountInfo(etc, "/elsewhere/upper", work) + }, + want: "private /etc overlay does not use this fixture's owned upper and work directories", + }, + { + name: "existing microsandbox dir", + args: args, + want: "managed-policy fixture requires /etc/microsandbox to be absent in the overlay lower layer", + setup: func(t *testing.T, etc string, _ *managedPolicyRuntime, fake *managedPolicyFake) { + fake.namespace = managedPolicyChildNS + fake.mountInfo = overlayMountInfo(etc, upper, work) + if err := os.Mkdir(filepath.Join(etc, "microsandbox"), 0o755); err != nil { + t.Fatal(err) + } + }, + }, + }) } func TestHasOverlayMount(t *testing.T) { - const line = "36 25 0:32 / /etc rw,relatime shared:1 - overlay overlay " + - "rw,lowerdir=/etc,upperdir=/s/upper,workdir=/s/work" + upper, work := "/fixture/upper", "/fixture/work" cases := []struct { - name string - mountInfo string - want bool + name, mountInfo string + want bool }{ - {name: "matching overlay", mountInfo: line, want: true}, - { - name: "upper and work on different lines", - mountInfo: "36 25 0:32 / /etc rw - overlay overlay upperdir=/s/upper\n" + - "37 25 0:33 / /etc rw - overlay overlay workdir=/s/work", - want: false, - }, - { - name: "other mount point", - mountInfo: strings.Replace(line, " /etc ", " /usr ", 1), - want: false, - }, + {"matching overlay", overlayMountInfo("/etc", upper, work), true}, { - name: "not an overlay", - mountInfo: strings.Replace(line, "overlay overlay", "tmpfs tmpfs", 1), - want: false, + "upper and work on different lines", + "36 25 0:31 / /etc rw - overlay overlay rw,upperdir=" + upper + "\n" + + "37 25 0:32 / /etc rw - overlay overlay rw,workdir=" + work, false, }, + {"other mount point", overlayMountInfo("/other", upper, work), false}, { - name: "different upper path", - mountInfo: strings.Replace(line, "/s/upper", "/x/upper", 1), - want: false, + "non-overlay fstype", + "36 25 8:1 / /etc rw - ext4 /dev/sda1 rw,upperdir=" + upper + ",workdir=" + work, + false, }, - {name: "truncated line", mountInfo: "36 25 /etc", want: false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - if got := hasOverlayMount(tc.mountInfo, "/etc", "/s/upper", "/s/work"); got != tc.want { - t.Fatalf("hasOverlayMount() = %t, want %t", got, tc.want) + if got := hasOverlayMount(tc.mountInfo, "/etc", upper, work); got != tc.want { + t.Fatalf("hasOverlayMount = %v, want %v", got, tc.want) } }) } } -func countEnvPrefix(env []string, prefix string) int { - count := 0 - for _, entry := range env { - if strings.HasPrefix(entry, prefix) { - count++ - } - } - return count -} - func TestManagedPolicyChildEnvironmentReplacesManagedKeys(t *testing.T) { + ns := managedPolicyNamespace{ + root: "/fixture", + parent: managedPolicyParentNS, + current: managedPolicyChildNS, + } environ := []string{ - managedPolicyEnvPath, - "RUNNER_TEMP=/rt", - "DEVSY_TEST_MSB_FIXTURE_ROOT=/stale", - "DEVSY_TEST_MSB_EXPECTED_UPPER=/stale", + managedPolicyPathEnv, + "DEVSY_TEST_MSB_EXPECTED_UPPER=/stale/upper", + managedPolicyMarker + "=false", } - ns := managedPolicyNamespace{root: "/r"} env := managedPolicyChildEnvironment(environ, ns, "/etc/microsandbox/managed.json") - - for _, keep := range []string{managedPolicyEnvPath, "RUNNER_TEMP=/rt"} { - if !slices.Contains(env, keep) { - t.Errorf("environment dropped %q", keep) - } - } - for _, stale := range []string{"DEVSY_TEST_MSB_FIXTURE_ROOT=/stale", "DEVSY_TEST_MSB_EXPECTED_UPPER=/stale"} { - if slices.Contains(env, stale) { - t.Errorf("environment kept stale %q", stale) + for _, want := range []string{ + managedPolicyPathEnv, + managedPolicyMarker + "=true", + "DEVSY_TEST_MSB_EXPECTED_UPPER=/fixture/upper", + "DEVSY_TEST_MSB_MANAGED_POLICY=/etc/microsandbox/managed.json", + } { + if !slices.Contains(env, want) { + t.Errorf("child env missing %q", want) } } - if !slices.Contains(env, "DEVSY_TEST_MSB_FIXTURE_ROOT=/r") { - t.Error("environment missing fixture root override") - } - if n := countEnvPrefix(env, "DEVSY_TEST_MSB_EXPECTED_UPPER="); n != 1 { - t.Fatalf("DEVSY_TEST_MSB_EXPECTED_UPPER entries = %d, want 1", n) + if slices.Contains(env, "DEVSY_TEST_MSB_EXPECTED_UPPER=/stale/upper") || + slices.Contains(env, managedPolicyMarker+"=false") { + t.Error("stale managed-policy value kept") } }