//go:build linux && rootrunner package host import ( "context" "encoding/json" "fmt" "io" "os" "os/exec" "path/filepath" "strings" "syscall" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootparticipation" "github.com/carlosframework/platform/internal/rootstate" "golang.org/x/sys/unix" ) func TestPlan14GenerationKernelNamespaceMountRootRunner(t *testing.T) { const childKey = "PLAN14_E27_NAMESPACE_CHILD" dir := os.Getenv(childKey) if dir == "" { dir = generationRootRunnerDir(t) cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", os.Args[0], "-test.run=^TestPlan14GenerationKernelNamespaceMountRootRunner$", "-test.v", "-test.timeout=2m") cmd.Dir = dir cmd.Env = append(os.Environ(), childKey+"="+dir) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("isolated namespace census: %v\n%s", err, out) } else { t.Logf("%s", out) } return } if os.Geteuid() != 0 { t.Fatal("designated runner must execute as root") } alias := filepath.Join(dir, "hidden-alias") tree := filepath.Join(dir, "versions") if os.Getenv("PLAN14_E27_NAMESPACE_HOLDER") == "1" { if err := unix.Mount("", "/", "", unix.MS_REC|unix.MS_PRIVATE, ""); err != nil { t.Fatal(err) } if err := unix.Mount(tree, alias, "", unix.MS_BIND, ""); err != nil { t.Fatal(err) } ready := os.NewFile(3, "ready") hold := os.NewFile(4, "hold") if _, err := ready.Write([]byte{1}); err != nil { t.Fatal(err) } ready.Close() if _, err := io.Copy(io.Discard, hold); err != nil { t.Fatal(err) } hold.Close() // Leave the bind in this private namespace for the parent's retained // namespace mount. The parent removes that final reference after checking. return } t.Setenv("CARLOS_OPT_DIR", dir) for _, path := range []string{tree, alias} { if err := os.Mkdir(path, 0755); err != nil { t.Fatal(err) } } if err := os.WriteFile(filepath.Join(tree, "artifact"), []byte("retained"), 0644); err != nil { t.Fatal(err) } pins, certificate := generationNamespacePinFixture(t, dir) if got, err := pins.StagedPins(t.Context(), dir); err != nil || len(got) != 0 { t.Fatalf("initial quiescent pins: %v %v", got, err) } initial := certificate() if !initial.Complete { t.Fatal("initial completeness was not recorded") } readyR, readyW, err := os.Pipe() if err != nil { t.Fatal(err) } defer readyR.Close() defer readyW.Close() holdR, holdW, err := os.Pipe() if err != nil { t.Fatal(err) } defer holdR.Close() defer holdW.Close() child := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^TestPlan14GenerationKernelNamespaceMountRootRunner$", "-test.timeout=90s") child.Env = append(os.Environ(), "PLAN14_E27_NAMESPACE_HOLDER=1") child.Dir = dir child.ExtraFiles = []*os.File{readyW, holdR} child.SysProcAttr = &syscall.SysProcAttr{Cloneflags: unix.CLONE_NEWNS} child.Stdout, child.Stderr = os.Stdout, os.Stderr if err := child.Start(); err != nil { t.Fatal(err) } joined := false defer func() { if !joined { _ = child.Process.Kill() _ = child.Wait() } }() readyW.Close() holdR.Close() var ready [1]byte if _, err := io.ReadFull(readyR, ready[:]); err != nil || ready[0] != 1 { t.Fatalf("namespace holder did not establish the bind: %v", err) } namespace := filepath.Join(dir, "retained-namespace") if err := os.WriteFile(namespace, nil, 0600); err != nil { t.Fatal(err) } if err := unix.Mount(fmt.Sprintf("/proc/%d/ns/mnt", child.Process.Pid), namespace, "", unix.MS_BIND, ""); err != nil { t.Fatal(err) } mounted := true defer func() { if mounted { if err := unix.Unmount(namespace, 0); err != nil { t.Error(err) } } }() holdW.Close() err = child.Wait() joined = true if err != nil { t.Fatal("namespace holder did not exit cleanly", err) } mounts, err := os.ReadFile("/proc/self/mountinfo") if err != nil { t.Fatal(err) } found := false for _, line := range strings.Split(string(mounts), "\n") { if strings.Contains(line, " "+namespace+" ") && strings.Contains(line, " - nsfs ") { found = true t.Logf("retained namespace after child exit: %s", line) } } if !found { t.Fatal("fixture did not retain an nsfs mount") } retained := exec.CommandContext(t.Context(), "nsenter", "--mount="+namespace, "/bin/cat", "/proc/self/mountinfo") if out, err := retained.CombinedOutput(); err != nil || !strings.Contains(string(out), " "+alias+" ") { t.Fatalf("retained namespace lost the staged bind: %v\n%s", err, out) } if got, err := pins.StagedPins(t.Context(), dir); err == nil || got != nil { t.Fatalf("namespace retained by a mount granted pins: %v %v", got, err) } denied := certificate() if denied.Complete || denied.Revision <= initial.Revision { t.Fatal("namespace refusal left the previous certificate usable") } if err := unix.Unmount(namespace, 0); err != nil { t.Fatal(err) } mounted = false if got, err := pins.StagedPins(t.Context(), dir); err != nil || len(got) != 0 { t.Fatalf("released namespace still refused: %v %v", got, err) } final := certificate() if !final.Complete || final.Revision <= denied.Revision { t.Fatal("released namespace did not obtain a fresh certificate") } } func generationNamespacePinFixture(t *testing.T, dir string) (generationPinSource, func() release.GenerationCompleteness) { t.Helper() censusPath, fencesPath := filepath.Join(dir, "census"), filepath.Join(dir, "fences") journalPath, generationsPath := filepath.Join(dir, "journals.db"), filepath.Join(dir, "generations") for _, path := range []string{censusPath, fencesPath, generationsPath} { state, err := rootstate.Open(path, true, false) if err != nil { t.Fatal(err) } if path == censusPath { raw, err := json.Marshal(rootparticipation.Census{Schema: 1, Box: "runner", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}) if err == nil { err = state.Write("census.json", raw) } if err != nil { state.Close() t.Fatal(err) } } state.Close() } journal, err := hostbox.OpenLaunchJournal(journalPath) if err != nil { t.Fatal(err) } _, err = hostbox.NewJournalService(journal) journal.Close() if err != nil { t.Fatal(err) } open := func() (rootstate.Store, error) { return rootstate.Open(generationsPath, false, false) } v := generationReconciler{ read: func(ctx context.Context) (generationInventory, error) { return generationInventoryAt(ctx, censusPath, fencesPath, journalPath) }, kernel: productionGenerationKernelCensus, } source := generationPinSource{open: open, retire: productionRetireGenerations, reconcile: v.reconcile} return source, func() release.GenerationCompleteness { t.Helper() state, err := open() if err != nil { t.Fatal(err) } defer state.Close() c, err := (&release.RootGenerations{Store: state}).Completeness() if err != nil { t.Fatal(err) } return c } }