package host import ( "context" "os" "path/filepath" "testing" "time" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) func TestPlan14GenerationDeletionCannotUnpin(t *testing.T) { for _, attack := range []string{"delete", "empty", "rename", "missing-completeness", "deleted-executable", "renamed-parent", "registry-deleted", "wrong-root", "sync-failure"} { t.Run(attack, func(t *testing.T) { f, disk, _ := generationLaunchFixture(t) permit, err := f.root.generations.reserve(t.Context(), f.root, f.peer, f.req) if err != nil { t.Fatal(err) } b := permit.reservation.Binding opt := f.root.generations.optDir stageSweepVersion(t, opt, "bap", "v2", "other", 48*time.Hour) stageSweepVersion(t, opt, "bap", "v3", "pending", 48*time.Hour) pending := b pending.Attempt, pending.Invocation = "pending-v3", "pending-v3" pending.Binding.Account, pending.Binding.App, pending.Version = "bap", "pending", "v3" pending.Path = release.VersionPath(opt, "bap", "v3", "pending") pending.Tree = filepath.Dir(pending.Path) for path, dst := range map[string]*rootstate.FileIdentity{pending.Path: &pending.Executable, pending.Tree: &pending.TreeIdentity} { file, err := os.Open(path) if err != nil { t.Fatal(err) } *dst, err = rootstate.Identity(file) file.Close() if err != nil { t.Fatal(err) } } if _, err := permit.ledger.Reserve(pending); err != nil { t.Fatal(err) } when := time.Now().Add(-48 * time.Hour) if err := os.Chtimes(b.Path, when, when); err != nil { t.Fatal(err) } opened, err := os.Open(b.Path) if err != nil { t.Fatal(err) } defer opened.Close() before, err := opened.Stat() if err != nil { t.Fatal(err) } dir := t.TempDir() path := release.GenerationsPath(dir) if err := release.WriteGenerations(path, release.Generations{Paths: map[string][]string{b.Binding.Host: {b.Path}}}); err != nil { t.Fatal(err) } source := generationPinSource{open: func() (rootstate.Store, error) { return disk, nil }, verify: func(context.Context, []release.GenerationReservation) error { return nil }} switch attack { case "empty": if err := release.WriteGenerations(path, release.Generations{}); err != nil { t.Fatal(err) } case "rename": if err := os.Rename(filepath.Dir(path), filepath.Dir(path)+".old"); err != nil { t.Fatal(err) } default: if err := os.Remove(path); err != nil { t.Fatal(err) } if err := os.Remove(release.GenerationsSeqPath(path)); err != nil { t.Fatal(err) } } switch attack { case "missing-completeness": source.verify = nil case "deleted-executable": if err := os.Remove(b.Path); err != nil { t.Fatal(err) } case "renamed-parent": if err := os.Rename(b.Tree, b.Tree+".old"); err != nil { t.Fatal(err) } case "wrong-root": opt = t.TempDir() stageSweepVersion(t, opt, "bab", "v1", "shop", 48*time.Hour) case "sync-failure": disk.fail = 1 } // No registry row or live link carries the root reservation. sweepStagedVersions(fakeLister{}, opt, path, 6*time.Hour, time.Now(), source) if attack == "renamed-parent" { if _, err := os.Stat(b.Tree + ".old"); err != nil { t.Fatalf("renamed reserved tree removed: %v", err) } } else if !staged(t, opt, "v1") { t.Fatal("sweep removed reserved version tree") } if attack != "wrong-root" && !staged(t, opt, "v3") { t.Fatal("pending V3 lost its independent pin") } after, err := opened.Stat() if err != nil || !os.SameFile(before, after) { t.Fatalf("held inode changed: %v", err) } if attack == "delete" || attack == "empty" || attack == "rename" || attack == "registry-deleted" { if staged(t, opt, "v2") { t.Fatal("complete fixture could not sweep unrelated version") } } else if attack != "wrong-root" && !staged(t, opt, "v2") { t.Fatal("incomplete snapshot permitted any deletion") } }) } } func TestPlan14GenerationConsumerMode(t *testing.T) { for _, legacy := range []bool{true, false} { calls := 0 source := generationPinSource{legacy: func() (bool, error) { return legacy, nil }, open: func() (rootstate.Store, error) { calls++; return nil, os.ErrPermission }} _, err := source.StagedPins(t.Context(), t.TempDir()) if legacy && (err != nil || calls != 0) { t.Fatalf("legacy read root generations: %v calls %d", err, calls) } if !legacy && (err == nil || calls != 1) { t.Fatalf("protected fallback: %v calls %d", err, calls) } } source := generationPinSource{legacy: func() (bool, error) { return true, os.ErrPermission }} if _, err := source.StagedPins(t.Context(), t.TempDir()); err == nil { t.Fatal("census error chose legacy") } } func TestPlan14GenerationProductionModeGate(t *testing.T) { for _, flag := range []string{"", "1"} { t.Run(flag, func(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", flag) source := productionRootGenerationPins().(generationPinSource) if source.legacy != nil { source.legacy = func() (bool, error) { return true, nil } } calls := 0 source.open = func() (rootstate.Store, error) { calls++; return nil, os.ErrPermission } opt := t.TempDir() stageSweepVersion(t, opt, "bab", "v1", "shop", 48*time.Hour) sweepStagedVersions(fakeLister{}, opt, noGenerations(t), 6*time.Hour, time.Now(), source) if calls != 0 || staged(t, opt, "v1") { t.Fatalf("no-census sweep changed: opens=%d survives=%v", calls, staged(t, opt, "v1")) } }) } }