//go:build linux package host import ( "context" "errors" "os" "path/filepath" "strconv" "strings" "syscall" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/stopevidence" ) // The kernel half without root: the fixture /proc and cgroup tree stand in // for the real ones (the root-only checks wrap this). func TestVerifyWritersAtUsesRootsOwnObservation(t *testing.T) { fx := newWritersKernelFixture(t) if _, err := fx.verify(t); err != nil { t.Fatal(err) } t.Run("a foreign holder", func(t *testing.T) { fx := newWritersKernelFixture(t) fx.holdDB(t, 30, 3) if _, err := fx.verify(t); !errors.Is(err, stopevidence.ErrForeignHolder) { t.Fatalf("got %v", err) } }) t.Run("the agent's unhinted descriptor", func(t *testing.T) { fx := newWritersKernelFixture(t) fx.hints = nil if _, err := fx.verify(t); !errors.Is(err, stopevidence.ErrForeignHolder) { t.Fatalf("got %v", err) } }) t.Run("a populated role cgroup", func(t *testing.T) { fx := newWritersKernelFixture(t) if err := os.WriteFile(filepath.Join(fx.cgroupRoot, fx.role, "cgroup.events"), []byte("populated 1\nfrozen 0\n"), 0o600); err != nil { t.Fatal(err) } if _, err := fx.verify(t); !errors.Is(err, ErrClaimStopInventory) { t.Fatalf("got %v", err) } }) t.Run("unsettled spawns", func(t *testing.T) { fx := newWritersKernelFixture(t) fx.journal.Settled = false if _, err := fx.verify(t); !errors.Is(err, ErrClaimStopInventory) { t.Fatalf("got %v", err) } }) t.Run("a pending role", func(t *testing.T) { fx := newWritersKernelFixture(t) fx.journal.PendingRoles = true if _, err := fx.verify(t); !errors.Is(err, ErrClaimStopInventory) { t.Fatalf("got %v", err) } }) t.Run("a replaced state directory", func(t *testing.T) { fx := newWritersKernelFixture(t) fx.inv.Entries[0].Inode++ if _, err := fx.verify(t); !errors.Is(err, ErrClaimStopInventory) { t.Fatalf("got %v", err) } }) } func TestVerifyNoProcessAt(t *testing.T) { fx := newWritersKernelFixture(t) fx.dropAgentHolds(t) if _, err := fx.noProcess(t, false); err != nil { t.Fatal(err) } t.Run("a holder of the database", func(t *testing.T) { fx := newWritersKernelFixture(t) if _, err := fx.noProcess(t, false); !errors.Is(err, ErrClaimStopIdentity) { t.Fatalf("got %v", err) } }) t.Run("a launch journal entry for the host", func(t *testing.T) { fx := newWritersKernelFixture(t) fx.dropAgentHolds(t) if _, err := fx.noProcess(t, true); !errors.Is(err, ErrClaimStopInventory) { t.Fatalf("got %v", err) } }) t.Run("a populated host cgroup", func(t *testing.T) { fx := newWritersKernelFixture(t) fx.dropAgentHolds(t) dir := filepath.Join(fx.agentCgroup, "carlos-exec@shop.example", "gen-1") if err := os.MkdirAll(dir, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "cgroup.events"), []byte("populated 1\n"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(filepath.Dir(dir), "cgroup.events"), []byte("populated 0\n"), 0o600); err != nil { t.Fatal(err) } if _, err := fx.noProcess(t, false); !errors.Is(err, ErrClaimStopInventory) { t.Fatalf("got %v", err) } }) t.Run("an empty host cgroup", func(t *testing.T) { fx := newWritersKernelFixture(t) fx.dropAgentHolds(t) dir := filepath.Join(fx.agentCgroup, "carlos-exec@shop.example") if err := os.MkdirAll(dir, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "cgroup.events"), []byte("populated 0\n"), 0o600); err != nil { t.Fatal(err) } ev, err := fx.noProcess(t, false) if err != nil || len(ev.CgroupIdentities) != 2 || !strings.HasPrefix(ev.CgroupIdentities[0], "empty:") || !strings.HasPrefix(ev.CgroupIdentities[1], "absent:") { t.Fatalf("%+v %v", ev, err) } }) } type writersKernelFixture struct { proc, cgroupRoot, dataRoot, agentCgroup, role string db string journal hostbox.StopKernelJournal inv StopInventory target hostguard.FenceTarget peer StopPeer hints []stopevidence.DescriptorHint } type fixedKernelJournal struct{ j hostbox.StopKernelJournal } func (f fixedKernelJournal) ReadStopKernelJournal(context.Context, hostguard.FenceTarget) (hostbox.StopKernelJournal, error) { return f.j, nil } type fixedLaunchHosts bool func (f fixedLaunchHosts) HostLaunches(_ context.Context, host string) ([]hostbox.LaunchInventory, error) { if !f { return nil, nil } return []hostbox.LaunchInventory{{Target: hostguard.FenceTarget{Host: host, Incarnation: "inc-1", Stamp: 11}}}, nil } func fileID(t *testing.T, path string) (uint64, uint64) { t.Helper() fi, err := os.Stat(path) if err != nil { t.Fatal(err) } st := fi.Sys().(*syscall.Stat_t) return uint64(st.Dev), st.Ino } func newWritersKernelFixture(t *testing.T) *writersKernelFixture { t.Helper() fx := &writersKernelFixture{proc: t.TempDir(), cgroupRoot: t.TempDir(), dataRoot: t.TempDir(), agentCgroup: t.TempDir(), role: "carlos/claim-1.local"} fx.target = hostguard.FenceTarget{Host: "shop.example", Incarnation: "inc-1", Stamp: 11, Session: 13, Box: "box-1", Enrollment: "enrollment-1", BootID: "boot-1", ControllerID: "controller-1"} fx.peer = StopPeer{UID: 1001, PID: 20, ProcessStart: "start-20", BootID: "boot-1", ControllerID: "controller-1"} var groups []hostbox.StopCgroupIdentity for _, rel := range []string{fx.role, fx.role + "/role-a"} { dir := filepath.Join(fx.cgroupRoot, rel) if err := os.MkdirAll(dir, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "cgroup.events"), []byte("populated 0\nfrozen 0\n"), 0o600); err != nil { t.Fatal(err) } dev, ino := fileID(t, dir) groups = append(groups, hostbox.StopCgroupIdentity{RelativePath: rel, Device: dev, Inode: ino}) } fx.journal = hostbox.StopKernelJournal{WritersClosed: true, V: 1, Target: fx.target, Settled: true, Cgroups: groups} state := filepath.Join(fx.dataRoot, "shop.example") if err := os.Mkdir(state, 0o700); err != nil { t.Fatal(err) } fx.db = filepath.Join(state, "data.db") for _, name := range []string{fx.db, fx.db + "-wal"} { if err := os.WriteFile(name, nil, 0o600); err != nil { t.Fatal(err) } } sdev, sino := fileID(t, state) fx.inv = StopInventory{Complete: true, Host: fx.target.Host, Incarnation: "inc-1", InstallationStamp: 11, Box: "box-1", Enrollment: "enrollment-1", Entries: []StopInventoryEntry{{Kind: "path", ManagedRoot: "data", RelativePath: "shop.example", Identity: inventoryIdentityFixture(t, fx.dataRoot, "shop.example"), Type: syscall.S_IFDIR, Device: sdev, Inode: sino, Materialization: "materialized"}}} fx.holdDB(t, 20, 7) dev, ino := fileID(t, fx.db) fx.hints = []stopevidence.DescriptorHint{{FD: 7, Device: dev, Inode: ino}} return fx } func (fx *writersKernelFixture) holdDB(t *testing.T, pid, fd int) { t.Helper() dir := filepath.Join(fx.proc, strconv.Itoa(pid), "task", strconv.Itoa(pid), "fd") if err := os.MkdirAll(dir, 0o700); err != nil { t.Fatal(err) } if err := os.Symlink(fx.db, filepath.Join(dir, strconv.Itoa(fd))); err != nil { t.Fatal(err) } stat := strconv.Itoa(pid) + " (p) S" + strings.Repeat(" 0", 18) + " start-" + strconv.Itoa(pid) + " 0\n" if err := os.WriteFile(filepath.Join(fx.proc, strconv.Itoa(pid), "stat"), []byte(stat), 0o600); err != nil { t.Fatal(err) } } func (fx *writersKernelFixture) dropAgentHolds(t *testing.T) { t.Helper() if err := os.Remove(filepath.Join(fx.proc, "20", "task", "20", "fd", "7")); err != nil { t.Fatal(err) } } func (fx *writersKernelFixture) roots() stopKernelRoots { return stopKernelRoots{Proc: fx.proc, Cgroup: fx.cgroupRoot, Managed: map[string]string{"data": fx.dataRoot}, AgentCgroup: fx.agentCgroup} } func (fx *writersKernelFixture) verify(t *testing.T) (WritersKernelEvidence, error) { return verifyWritersAt(t.Context(), fx.roots(), fixedKernelJournal{fx.journal}, fx.target, fx.inv, fx.peer, fx.hints) } func (fx *writersKernelFixture) noProcess(t *testing.T, has bool) (StopKernelEvidence, error) { target := fx.target target.Session = 0 return verifyNoProcessAt(t.Context(), fx.roots(), fixedLaunchHosts(has), target, fx.inv) } var _ = errors.Is