//go:build linux && rootrunner package host import ( "errors" "os" "path/filepath" "reflect" "testing" "time" "github.com/carlosframework/platform/internal/rootstate" "golang.org/x/sys/unix" ) // Parent authority is a fixture. UID, mount checks, descriptor-relative rename // and the private store are production; no agent or running root service is used. func TestRootServingParkQuarantine(t *testing.T) { runRootServingParkQuarantine(t, []string{"", "lost-rename-reply", "submount", "symlink"}) } func TestRootServingParkUnrecordedReplacement(t *testing.T) { runRootServingParkQuarantine(t, []string{"lost-fresh-record", "lost-retention-reply", "unrecorded-submount", "repeated-full-disk"}) } func runRootServingParkQuarantine(t *testing.T, faults []string) { t.Helper() servingRootRunner(t) for _, fault := range faults { t.Run(fault, func(t *testing.T) { root, err := os.MkdirTemp("/var/lib", "carlos-rootrunner-quarantine-") if err != nil { t.Fatal(err) } t.Cleanup(func() { if err := os.RemoveAll(root); err != nil { t.Error(err) } }) data := filepath.Join(root, "data") servingRunnerMount(t, data) state, err := rootstate.Open(filepath.Join(root, "authority"), true, false) if err != nil { t.Fatal(err) } t.Cleanup(func() { if err := state.Close(); err != nil { t.Error(err) } }) fs := newServingParkFS(data, state) p, parent, err := fs.parent() if err != nil { t.Fatal(err) } p.Close() path := filepath.Join(data, "instances", ".serving-parked") if err := os.Mkdir(path, 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(path, "unknown"), []byte("retained unknown bytes"), 0600); err != nil { t.Fatal(err) } if fault == "submount" { if err := unix.Mount(path, path, "", unix.MS_BIND, ""); err != nil { t.Fatal(err) } t.Cleanup(func() { if err := unix.Unmount(path, 0); err != nil { t.Error(err) } }) } if fault == "symlink" { if err := os.Rename(path, path+"-saved"); err != nil { t.Fatal(err) } if err := os.Symlink(path+"-saved", path); err != nil { t.Fatal(err) } } ledger := servingParkLedger{state: state, fs: fs, now: time.Now} recover := func() (servingParkQuarantine, error) { var q servingParkQuarantine err := ledger.withNamespace(t.Context(), func() error { var err error; q, err = fs.recoverNamespace(t.Context(), parent); return err }) return q, err } if fault == "lost-rename-reply" { fs.rename = func(a int, b string, c int, d string, flags uint) error { if err := unix.Renameat2(a, b, c, d, flags); err != nil { return err } return unix.EIO } if _, err := recover(); !errors.Is(err, unix.EIO) { t.Fatal("lost reply", err) } fs.rename = unix.Renameat2 } var unrecorded servingArtifactDirectory if fault == "lost-fresh-record" || fault == "lost-retention-reply" || fault == "unrecorded-submount" || fault == "repeated-full-disk" { fs.state = quarantineWriteFault{Store: state, before: func(name string, body []byte) error { var q servingParkQuarantine if name == "namespace-quarantine.json" && servingParkDecode(body, &q) == nil && q.Fresh.Inode != 0 { return unix.ENOSPC } return nil }} if _, err := recover(); !errors.Is(err, unix.ENOSPC) { t.Fatal("missing fresh identity write failure", err) } d, err := os.Open(path) if err != nil { t.Fatal(err) } unrecorded, err = servingParkDirID(d) d.Close() if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(path, "uncertain"), []byte("unrecorded bytes"), 0600); err != nil { t.Fatal(err) } if fault == "repeated-full-disk" { if _, err := recover(); !errors.Is(err, unix.ENOSPC) { t.Fatal("missing second full disk failure", err) } } fs.state = state if fault == "lost-retention-reply" { fs.rename = func(a int, b string, c int, d string, flags uint) error { if err := unix.Renameat2(a, b, c, d, flags); err != nil { return err } return unix.EIO } if _, err := recover(); !errors.Is(err, unix.EIO) { t.Fatal("missing retention reply failure", err) } } fs = newServingParkFS(data, state) if fault == "unrecorded-submount" { if err := unix.Mount(path, path, "", unix.MS_BIND, ""); err != nil { t.Fatal(err) } t.Cleanup(func() { if err := unix.Unmount(path, 0); err != nil { t.Error(err) } }) } } q, err := recover() if fault == "unrecorded-submount" || fault == "repeated-full-disk" { if err == nil { t.Fatal("unrecorded replacement accepted", fault) } if _, err := state.Read("namespace.json"); !errors.Is(err, os.ErrNotExist) { t.Fatal("unsafe namespace published", err) } retained := path if fault == "repeated-full-disk" { retained = filepath.Join(filepath.Dir(path), q.unrecordedName()) } body, err := os.ReadFile(filepath.Join(retained, "uncertain")) if err != nil || string(body) != "unrecorded bytes" { t.Fatal("refusal lost unrecorded bytes", err) } return } if fault == "submount" || fault == "symlink" { if err == nil { t.Fatal("unsafe obstruction accepted") } if _, err := state.Read("namespace-quarantine.json"); !errors.Is(err, os.ErrNotExist) { t.Fatal("unsafe quarantine intent", err) } return } if err != nil || !q.Complete { t.Fatalf("quarantine: %+v %v", q, err) } if q.Obstruction.Mount != parent.Mount || q.Fresh.Mount != parent.Mount { t.Fatal("quarantine crossed bind mount") } if unrecorded.Inode != 0 { if q.Unrecorded != unrecorded || !q.UnrecordedMoved || q.Fresh == unrecorded { t.Fatal("unrecorded root became namespace authority") } body, err := os.ReadFile(filepath.Join(filepath.Dir(path), q.unrecordedName(), "uncertain")) if err != nil || string(body) != "unrecorded bytes" { t.Fatal("retention changed unrecorded bytes", err) } } body, err := os.ReadFile(filepath.Join(data, "instances", q.name(), "unknown")) if err != nil || string(body) != "retained unknown bytes" { t.Fatal("quarantine changed bytes", err) } fs = newServingParkFS(data, state) if replay, err := recover(); err != nil || !reflect.DeepEqual(q, replay) { t.Fatal("fresh-adapter replay", err) } r := servingRunnerPark(t, data, state) if err := fs.CheckParked(&r); err != nil { t.Fatal("park after quarantine", err) } body, err = os.ReadFile(filepath.Join(data, "instances", q.name(), "unknown")) if err != nil || string(body) != "retained unknown bytes" { t.Fatal("park touched quarantine", err) } }) } }