//go:build linux && rootrunner package host import ( "context" "encoding/json" "os" "os/exec" "path/filepath" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" "golang.org/x/sys/unix" ) func TestPlan14GenerationParkedRootRunner(t *testing.T) { const child = "PLAN14_E38_PARKED_ROOT" dir := os.Getenv(child) if dir == "" { generationRunnerRegistryUser(t) var err error dir, err = os.MkdirTemp("/run", "plan14-parked-") if err != nil { t.Fatal(err) } t.Cleanup(func() { if err := os.RemoveAll(dir); err != nil { t.Error(err) } }) if err := os.Chmod(dir, 0755); err != nil { t.Fatal(err) } binary, err := os.Executable() if err != nil { t.Fatal(err) } cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", binary, "-test.run=^TestPlan14GenerationParkedRootRunner$", "-test.v", "-test.timeout=2m") cmd.Dir = dir cmd.Env = append(os.Environ(), child+"="+dir, "TMPDIR="+dir) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("isolated parked inventory: %v\n%s", err, out) } t.Logf("%s", out) return } if os.Geteuid() != 0 { t.Fatal("designated runner requires root") } if err := unix.Mount("", "/", "", unix.MS_REC|unix.MS_PRIVATE, ""); err != nil { t.Fatal(err) } private := filepath.Join(dir, "varlib") if err := os.Mkdir(private, 0755); err != nil { t.Fatal(err) } if err := unix.Mount(private, "/var/lib", "", unix.MS_BIND, ""); err != nil { t.Fatal(err) } t.Cleanup(func() { if err := unix.Unmount("/var/lib", 0); err != nil { t.Error(err) } }) snapshot, j, root := generationParkedFixture(t) if os.Getenv("PLAN14_R11_DELETION") == "1" { j.Target.ClaimETag = "fixture-record" var receipt StopReceipt if err := json.Unmarshal(j.StopReceipt, &receipt); err != nil { t.Fatal(err) } receipt.ClaimETag = j.Target.ClaimETag j.StopReceipt = mustMarshalStopReceipt(receipt) snapshot.Installations[0].Receipts[0].Bytes = j.StopReceipt snapshot.Installations[0].Receipts[0].Target = *j.Target snapshot.Launches[0].Target = *j.Target } t.Setenv("CARLOS_HOST_CLAIM", "true") t.Setenv("CARLOS_OPT_DIR", dir) if err := os.Mkdir(filepath.Join(dir, "versions"), 0755); err != nil { t.Fatal(err) } for _, path := range []string{filepath.Dir(root), root} { if err := os.Chmod(path, 0755); err != nil { t.Fatal(err) } } reg, err := registry.Open(filepath.Join(root, "registry.db")) if err != nil { t.Fatal(err) } owner := registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"} if err := reg.Upsert(registry.Route{Host: j.Inventory.Host, ClaimOwnership: owner}); err != nil { t.Fatal(err) } owner.ClaimRowID = "timer" if err := reg.ReplaceTimers(j.Inventory.Host, []registry.Timer{{Host: j.Inventory.Host, Name: "backup", Kind: "cron", Registration: "timer", ClaimOwnership: owner}}); err != nil { t.Fatal(err) } // The installation, closed launch and stop receipt are fixtures. Parking, // conditional row retirement, checked readback and kernel scanning are real. var deletedStore release.Store if os.Getenv("PLAN14_R11_DELETION") == "1" { deployment := filepath.Join(dir, "deployment") t.Setenv("CARLOS_DEPLOYMENT_DIR", deployment) t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "") deletedStore = release.NewDirStore(deployment) in := snapshot.Installations[0].Installation record := provision.Record{Schema: 1, Host: in.Host, Account: in.Account, App: in.App, Channel: "stable"} if err := provision.CreateRecord(t.Context(), deletedStore, record); err != nil { t.Fatal(err) } if err := deletedStore.Delete(t.Context(), in.Record); err != nil { t.Fatal(err) } } generationRunPark(t, root, j, reg, "") if err := reg.Close(); err != nil { t.Fatal(err) } uid, gid := generationRunnerRegistryUser(t) if err := os.Chown(filepath.Join(root, "registry.db"), uid, gid); err != nil { t.Fatal(err) } if err := os.Chown(root, uid, gid); err != nil { t.Fatal(err) } path := claimRetireJournalDir("", "") state, err := rootstate.Open(path, true, false) if err != nil { t.Fatal(err) } defer state.Close() raw, err := os.ReadFile(filepath.Join(root, "retire", j.ID+".json")) if err != nil { t.Fatal(err) } if err := state.Write(j.ID+".json", raw); err != nil { t.Fatal(err) } if deletedStore != nil { r11DeletedRecordPins(t, snapshot, deletedStore) return } read := func(ctx context.Context) (generationInventory, error) { journals, err := productionGenerationRetirements(ctx) if err != nil { return generationInventory{}, err } return productionGenerationRegistryInventory(ctx, generationParkedSnapshot(t, snapshot, journals)) } v := generationReconciler{read: read, kernel: productionGenerationKernelCensus} accept := func() { t.Helper() if _, err := v.reconcile(t.Context(), nil); err != nil { t.Fatal("checked parked reconciliation", err) } } accept() for _, attack := range []string{"uid", "writable-file", "symlink", "writable-parent", "missing-journal", "pending", "parked-replacement"} { t.Run(attack, func(t *testing.T) { file := filepath.Join(path, j.ID+".json") switch attack { case "uid": if err := os.Chown(file, uid, gid); err != nil { t.Fatal(err) } t.Cleanup(func() { if err := os.Chown(file, 0, 0); err != nil { t.Error(err) } }) case "writable-file": if err := os.Chmod(file, 0666); err != nil { t.Fatal(err) } t.Cleanup(func() { if err := os.Chmod(file, 0600); err != nil { t.Error(err) } }) case "writable-parent": if err := os.Chmod(filepath.Dir(path), 0777); err != nil { t.Fatal(err) } t.Cleanup(func() { if err := os.Chmod(filepath.Dir(path), 0700); err != nil { t.Error(err) } }) case "symlink", "missing-journal": if err := os.Rename(file, file+".saved"); err != nil { t.Fatal(err) } if attack == "symlink" { if err := os.Symlink(file+".saved", file); err != nil { t.Fatal(err) } } t.Cleanup(func() { if attack == "symlink" { if err := os.Remove(file); err != nil { t.Error(err) } } if err := os.Rename(file+".saved", file); err != nil { t.Error(err) } }) case "pending": var pending hostbox.RetirementJournal if err := json.Unmarshal(raw, &pending); err != nil { t.Fatal(err) } pending.Complete = false body, err := json.Marshal(pending) if err != nil { t.Fatal(err) } if err := state.Write(j.ID+".json", body); err != nil { t.Fatal(err) } t.Cleanup(func() { if err := state.Write(j.ID+".json", raw); err != nil { t.Error(err) } }) case "parked-replacement": parked := filepath.Join(root, ".parked", "inc", j.Inventory.Host, j.ID, "data", "instances", j.Inventory.Host) if err := os.Rename(parked, parked+".saved"); err != nil { t.Fatal(err) } if err := os.Mkdir(parked, 0700); err != nil { t.Fatal(err) } t.Cleanup(func() { if err := os.Remove(parked); err != nil { t.Error(err) } if err := os.Rename(parked+".saved", parked); err != nil { t.Error(err) } }) } if _, err := v.reconcile(t.Context(), nil); err == nil { t.Fatal("hostile parked state certified") } }) accept() } }