//go:build linux && rootrunner package host import ( "encoding/json" "os" "path/filepath" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/rootparticipation" "github.com/carlosframework/platform/internal/rootstate" ) func TestPlan14GenerationInventoryCheckedRootReader(t *testing.T) { dir := generationRootRunnerDir(t) censusPath, fencesPath, journalPath := filepath.Join(dir, "census"), filepath.Join(dir, "fences"), filepath.Join(dir, "journals.db") census, err := rootstate.Open(censusPath, true, false) if err != nil { t.Fatal(err) } defer census.Close() fences, err := rootstate.Open(fencesPath, true, false) if err != nil { t.Fatal(err) } fences.Close() c := rootparticipation.Census{Schema: 1, Box: "runner", Enrollment: "runner-en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true} raw, err := json.Marshal(c) if err != nil { t.Fatal(err) } if err := census.Write("census.json", raw); err != nil { t.Fatal(err) } journal, err := hostbox.OpenLaunchJournal(journalPath) if err != nil { t.Fatal(err) } service, err := hostbox.NewJournalService(journal) if err != nil { t.Fatal(err) } in := hostbox.StopInventory{Complete: true, Host: "retained.example", Incarnation: "inc", InstallationStamp: 2, Box: c.Box, Enrollment: c.Enrollment, DirectoryRevision: "1"} if err := service.Inventory.WriteStopInventory(t.Context(), in); err != nil { t.Fatal(err) } journal.Close() got, err := generationInventoryAt(t.Context(), censusPath, fencesPath, journalPath) if err != nil || got.Census != c || len(got.Installations) != 0 || len(got.Launches) != 0 || len(got.Payloads) != 1 || got.Payloads[0].Revision != 1 || got.Payloads[0].Inventory.Host != in.Host { t.Fatalf("checked snapshot: %+v %v", got, err) } // An independently discovered payload without its installation cannot be // certified, even though the payload itself has Complete set. if err := generationPayloadBindings(got.Payloads, nil); err == nil { t.Fatal("orphan payload granted completeness") } for _, attack := range []string{"writable-db", "service-db", "symlink-db", "writable-wal", "service-wal", "missing-db", "missing-census"} { t.Run(attack, func(t *testing.T) { switch attack { case "writable-db": if err := os.Chmod(journalPath, 0666); err != nil { t.Fatal(err) } defer os.Chmod(journalPath, 0600) case "service-db": if err := os.Chown(journalPath, 65534, 65534); err != nil { t.Fatal(err) } defer os.Chown(journalPath, 0, 0) case "symlink-db": if err := os.Rename(journalPath, journalPath+".saved"); err != nil { t.Fatal(err) } defer func() { os.Remove(journalPath); os.Rename(journalPath+".saved", journalPath) }() if err := os.Symlink(journalPath+".saved", journalPath); err != nil { t.Fatal(err) } case "writable-wal", "service-wal": wal := journalPath + "-wal" if err := os.WriteFile(wal, []byte("hostile"), 0600); err != nil { t.Fatal(err) } defer os.Remove(wal) if attack == "writable-wal" { if err := os.Chmod(wal, 0666); err != nil { t.Fatal(err) } } else { if err := os.Chown(wal, 65534, 65534); err != nil { t.Fatal(err) } } case "missing-db": if err := os.Rename(journalPath, journalPath+".saved"); err != nil { t.Fatal(err) } defer os.Rename(journalPath+".saved", journalPath) case "missing-census": if err := census.Remove("census.json"); err != nil { t.Fatal(err) } defer census.Write("census.json", raw) } if _, err := generationInventoryAt(t.Context(), censusPath, fencesPath, journalPath); err == nil { t.Fatal("hostile source accepted") } }) } } func generationRootRunnerDir(t *testing.T) string { t.Helper() if os.Geteuid() != 0 { t.Fatal("designated runner must execute as root") } dir, err := os.MkdirTemp("/var/lib", "plan14-generation-") 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) } check, err := rootstate.Open(dir, false, true) if err != nil { t.Fatal(err) } check.Close() return dir }