package release import ( "context" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/rootstate" ) type generationListingStore struct{ *healthyRetryStore } func (s generationListingStore) Names() ([]string, error) { entries, err := os.ReadDir(s.dir) var names []string for _, e := range entries { names = append(names, e.Name()) } return names, err } func generationPinFixture(t *testing.T, opt, account, app, version string) (*RootGenerations, GenerationReservation) { t.Helper() g, s, b := generationFixture(t) g.Store = generationListingStore{s} boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id") if err != nil { t.Fatal(err) } b.Target.BootID = strings.TrimSpace(string(boot)) b.Binding.Account, b.Binding.App, b.Version = account, app, version b.Path = VersionPath(opt, account, version, app) b.Tree = filepath.Dir(b.Path) for path, dst := range map[string]*rootstate.FileIdentity{b.Path: &b.Executable, b.Tree: &b.TreeIdentity} { f, err := os.Open(path) if err != nil { t.Fatal(err) } current, e := rootstate.ObjectIdentity(f) if e != nil { f.Close() t.Fatal(e) } b.Target.BootID = current.Boot *dst, err = rootstate.Identity(f) f.Close() if err != nil { t.Fatal(err) } } r, err := g.Reserve(b) if err != nil { t.Fatal(err) } return g, r } func TestPlan14GenerationDeletionCannotUnpin(t *testing.T) { for _, attack := range []string{"delete", "empty", "rename", "missing-completeness", "deleted-executable", "renamed-parent", "changed-inode"} { t.Run(attack, func(t *testing.T) { f := newReuseFixture(t) g, r := generationPinFixture(t, f.opt, "bab", "myapp", "v2") dir := t.TempDir() path := GenerationsPath(dir) if err := WriteGenerations(path, Generations{Paths: map[string][]string{"erased.example": {r.Binding.Path}}}); err != nil { t.Fatal(err) } before, err := os.Stat(r.Binding.Path) if err != nil { t.Fatal(err) } opened, err := os.Open(r.Binding.Path) if err != nil { t.Fatal(err) } defer opened.Close() source := RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }} switch attack { case "empty": if err := WriteGenerations(path, 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(GenerationsSeqPath(path)); err != nil { t.Fatal(err) } } switch attack { case "missing-completeness": source.Verify = nil case "deleted-executable": if err := os.Remove(r.Binding.Path); err != nil { t.Fatal(err) } case "renamed-parent": if err := os.Rename(r.Binding.Tree, r.Binding.Tree+".old"); err != nil { t.Fatal(err) } case "changed-inode": if err := os.Rename(r.Binding.Path, r.Binding.Path+".old"); err != nil { t.Fatal(err) } if err := os.WriteFile(r.Binding.Path, []byte("replacement"), 0755); err != nil { t.Fatal(err) } } f.pass(t, BakeDir(dir), WithRootGenerationPins(source)) got := make([]byte, len("old v2")) if _, err := opened.ReadAt(got, 0); err != nil || string(got) != "old v2" { t.Fatalf("reserved bytes changed: %q %v", got, err) } if attack == "deleted-executable" { if _, err := os.Stat(r.Binding.Path); !os.IsNotExist(err) { t.Fatalf("missing pinned path restaged: %v", err) } } else if attack != "changed-inode" && attack != "renamed-parent" { after, err := os.Stat(r.Binding.Path) if err != nil || !os.SameFile(before, after) { t.Fatalf("reserved inode replaced: %v", err) } } if attack == "renamed-parent" { after, err := os.Stat(filepath.Join(r.Binding.Tree+".old", filepath.Base(r.Binding.Path))) if err != nil || !os.SameFile(before, after) { t.Fatalf("renamed reservation destroyed: %v", err) } return } if v, _ := LiveVersion(f.opt, "h1.example.com"); v != "v1" { t.Fatalf("refused staging adopted %s", v) } }) } } func TestPlan14GenerationSnapshotCompleteness(t *testing.T) { f := newReuseFixture(t) g, r := generationPinFixture(t, f.opt, "bab", "myapp", "v2") source := RootGenerationPins{Ledger: g} if _, err := source.StagedPins(t.Context(), f.opt); err == nil { t.Fatal("missing completeness granted pins") } called := false source.Verify = func(_ context.Context, records []GenerationReservation) error { called = true if len(records) != 1 || records[0] != r { t.Fatalf("wrong root inventory: %+v", records) } return nil } pins, err := source.StagedPins(t.Context(), f.opt) if err != nil || !called || !pins.Has(r.Binding.Path) { t.Fatalf("snapshot: %v %v", pins, err) } s := g.Store.(generationListingStore) s.fail = 1 if _, err := source.StagedPins(t.Context(), f.opt); err == nil { t.Fatal("snapshot ignored failed directory sync") } if _, err := source.StagedPins(t.Context(), f.opt); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(s.dir, "foreign.json"), []byte(`{}`), 0600); err != nil { t.Fatal(err) } if _, err := source.StagedPins(t.Context(), f.opt); err == nil { t.Fatal("malformed inventory granted snapshot") } } func TestPlan14GenerationSnapshotPermitsUnreservedRestage(t *testing.T) { f := newReuseFixture(t) g, _ := generationPinFixture(t, f.opt, "bab", "myapp", "v1") source := RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }} f.pass(t, WithRootGenerationPins(source)) if got := f.stagedV2(t); got != "new v2" { t.Fatalf("unreserved copy not restaged: %q", got) } } func TestPlan14GenerationSnapshotPinsContainingTree(t *testing.T) { f := newReuseFixture(t) if err := StageArtifact(f.opt, "bab", "v2", "sibling", []byte("sibling")); err != nil { t.Fatal(err) } g, _ := generationPinFixture(t, f.opt, "bab", "sibling", "v2") source := RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }} f.pass(t, WithRootGenerationPins(source)) if got := f.stagedV2(t); got != "old v2" { t.Fatalf("reserved containing tree changed: %q", got) } } func TestPlan14GenerationSnapshotEmptyIsUnknown(t *testing.T) { g, _, _ := generationFixture(t) s := g.Store.(*healthyRetryStore) g.Store = generationListingStore{s} if _, err := (RootGenerationPins{Ledger: g}).StagedPins(t.Context(), t.TempDir()); err == nil { t.Fatal("empty reservation directory established completeness") } } func TestPlan14GenerationSnapshotPermitsFreshTree(t *testing.T) { f := newReuseFixture(t) tree := filepath.Join(f.opt, "versions", "v2") if err := os.RemoveAll(tree); err != nil { t.Fatal(err) } reads := 0 source := PinSourceFunc(func(context.Context, string) (StagedPaths, error) { reads++; return nil, os.ErrPermission }) f.pass(t, WithRootGenerationPins(source)) if got := f.stagedV2(t); got != "new v2" || reads != 0 { t.Fatalf("fresh signed build blocked: %q reads=%d", got, reads) } } func TestPlan14GenerationLegacyPinBootBoundary(t *testing.T) { f := newReuseFixture(t) g, r := generationPinFixture(t, f.opt, "bab", "myapp", "v2") source := RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }} if _, err := source.StagedPins(t.Context(), f.opt); err != nil { t.Fatal("same boot pin refused", err) } r.Binding.Target.BootID = "previous-boot" g.Store = generationListingStore{&healthyRetryStore{dir: t.TempDir()}} if _, err := g.Reserve(r.Binding); err != nil { t.Fatal(err) } if _, err := source.StagedPins(t.Context(), f.opt); err == nil { t.Fatal("old boot pin guessed from device number") } }