carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "github.com/carlosframework/platform/internal/rootstate"
11)
12
13type generationListingStore struct{ *healthyRetryStore }
14
15func (s generationListingStore) Names() ([]string, error) {
16 entries, err := os.ReadDir(s.dir)
17 var names []string
18 for _, e := range entries {
19 names = append(names, e.Name())
20 }
21 return names, err
22}
23
24func generationPinFixture(t *testing.T, opt, account, app, version string) (*RootGenerations, GenerationReservation) {
25 t.Helper()
26 g, s, b := generationFixture(t)
27 g.Store = generationListingStore{s}
28 boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
29 if err != nil {
30 t.Fatal(err)
31 }
32 b.Target.BootID = strings.TrimSpace(string(boot))
33 b.Binding.Account, b.Binding.App, b.Version = account, app, version
34 b.Path = VersionPath(opt, account, version, app)
35 b.Tree = filepath.Dir(b.Path)
36 for path, dst := range map[string]*rootstate.FileIdentity{b.Path: &b.Executable, b.Tree: &b.TreeIdentity} {
37 f, err := os.Open(path)
38 if err != nil {
39 t.Fatal(err)
40 }
41 current, e := rootstate.ObjectIdentity(f)
42 if e != nil {
43 f.Close()
44 t.Fatal(e)
45 }
46 b.Target.BootID = current.Boot
47 *dst, err = rootstate.Identity(f)
48 f.Close()
49 if err != nil {
50 t.Fatal(err)
51 }
52 }
53 r, err := g.Reserve(b)
54 if err != nil {
55 t.Fatal(err)
56 }
57 return g, r
58}
59
60func TestPlan14GenerationDeletionCannotUnpin(t *testing.T) {
61 for _, attack := range []string{"delete", "empty", "rename", "missing-completeness", "deleted-executable", "renamed-parent", "changed-inode"} {
62 t.Run(attack, func(t *testing.T) {
63 f := newReuseFixture(t)
64 g, r := generationPinFixture(t, f.opt, "bab", "myapp", "v2")
65 dir := t.TempDir()
66 path := GenerationsPath(dir)
67 if err := WriteGenerations(path, Generations{Paths: map[string][]string{"erased.example": {r.Binding.Path}}}); err != nil {
68 t.Fatal(err)
69 }
70 before, err := os.Stat(r.Binding.Path)
71 if err != nil {
72 t.Fatal(err)
73 }
74 opened, err := os.Open(r.Binding.Path)
75 if err != nil {
76 t.Fatal(err)
77 }
78 defer opened.Close()
79 source := RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }}
80 switch attack {
81 case "empty":
82 if err := WriteGenerations(path, Generations{}); err != nil {
83 t.Fatal(err)
84 }
85 case "rename":
86 if err := os.Rename(filepath.Dir(path), filepath.Dir(path)+".old"); err != nil {
87 t.Fatal(err)
88 }
89 default:
90 if err := os.Remove(path); err != nil {
91 t.Fatal(err)
92 }
93 if err := os.Remove(GenerationsSeqPath(path)); err != nil {
94 t.Fatal(err)
95 }
96 }
97 switch attack {
98 case "missing-completeness":
99 source.Verify = nil
100 case "deleted-executable":
101 if err := os.Remove(r.Binding.Path); err != nil {
102 t.Fatal(err)
103 }
104 case "renamed-parent":
105 if err := os.Rename(r.Binding.Tree, r.Binding.Tree+".old"); err != nil {
106 t.Fatal(err)
107 }
108 case "changed-inode":
109 if err := os.Rename(r.Binding.Path, r.Binding.Path+".old"); err != nil {
110 t.Fatal(err)
111 }
112 if err := os.WriteFile(r.Binding.Path, []byte("replacement"), 0755); err != nil {
113 t.Fatal(err)
114 }
115 }
116 f.pass(t, BakeDir(dir), WithRootGenerationPins(source))
117 got := make([]byte, len("old v2"))
118 if _, err := opened.ReadAt(got, 0); err != nil || string(got) != "old v2" {
119 t.Fatalf("reserved bytes changed: %q %v", got, err)
120 }
121 if attack == "deleted-executable" {
122 if _, err := os.Stat(r.Binding.Path); !os.IsNotExist(err) {
123 t.Fatalf("missing pinned path restaged: %v", err)
124 }
125 } else if attack != "changed-inode" && attack != "renamed-parent" {
126 after, err := os.Stat(r.Binding.Path)
127 if err != nil || !os.SameFile(before, after) {
128 t.Fatalf("reserved inode replaced: %v", err)
129 }
130 }
131 if attack == "renamed-parent" {
132 after, err := os.Stat(filepath.Join(r.Binding.Tree+".old", filepath.Base(r.Binding.Path)))
133 if err != nil || !os.SameFile(before, after) {
134 t.Fatalf("renamed reservation destroyed: %v", err)
135 }
136 return
137 }
138 if v, _ := LiveVersion(f.opt, "h1.example.com"); v != "v1" {
139 t.Fatalf("refused staging adopted %s", v)
140 }
141 })
142 }
143}
144
145func TestPlan14GenerationSnapshotCompleteness(t *testing.T) {
146 f := newReuseFixture(t)
147 g, r := generationPinFixture(t, f.opt, "bab", "myapp", "v2")
148 source := RootGenerationPins{Ledger: g}
149 if _, err := source.StagedPins(t.Context(), f.opt); err == nil {
150 t.Fatal("missing completeness granted pins")
151 }
152 called := false
153 source.Verify = func(_ context.Context, records []GenerationReservation) error {
154 called = true
155 if len(records) != 1 || records[0] != r {
156 t.Fatalf("wrong root inventory: %+v", records)
157 }
158 return nil
159 }
160 pins, err := source.StagedPins(t.Context(), f.opt)
161 if err != nil || !called || !pins.Has(r.Binding.Path) {
162 t.Fatalf("snapshot: %v %v", pins, err)
163 }
164 s := g.Store.(generationListingStore)
165 s.fail = 1
166 if _, err := source.StagedPins(t.Context(), f.opt); err == nil {
167 t.Fatal("snapshot ignored failed directory sync")
168 }
169 if _, err := source.StagedPins(t.Context(), f.opt); err != nil {
170 t.Fatal(err)
171 }
172 if err := os.WriteFile(filepath.Join(s.dir, "foreign.json"), []byte(`{}`), 0600); err != nil {
173 t.Fatal(err)
174 }
175 if _, err := source.StagedPins(t.Context(), f.opt); err == nil {
176 t.Fatal("malformed inventory granted snapshot")
177 }
178}
179
180func TestPlan14GenerationSnapshotPermitsUnreservedRestage(t *testing.T) {
181 f := newReuseFixture(t)
182 g, _ := generationPinFixture(t, f.opt, "bab", "myapp", "v1")
183 source := RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }}
184 f.pass(t, WithRootGenerationPins(source))
185 if got := f.stagedV2(t); got != "new v2" {
186 t.Fatalf("unreserved copy not restaged: %q", got)
187 }
188}
189
190func TestPlan14GenerationSnapshotPinsContainingTree(t *testing.T) {
191 f := newReuseFixture(t)
192 if err := StageArtifact(f.opt, "bab", "v2", "sibling", []byte("sibling")); err != nil {
193 t.Fatal(err)
194 }
195 g, _ := generationPinFixture(t, f.opt, "bab", "sibling", "v2")
196 source := RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }}
197 f.pass(t, WithRootGenerationPins(source))
198 if got := f.stagedV2(t); got != "old v2" {
199 t.Fatalf("reserved containing tree changed: %q", got)
200 }
201}
202
203func TestPlan14GenerationSnapshotEmptyIsUnknown(t *testing.T) {
204 g, _, _ := generationFixture(t)
205 s := g.Store.(*healthyRetryStore)
206 g.Store = generationListingStore{s}
207 if _, err := (RootGenerationPins{Ledger: g}).StagedPins(t.Context(), t.TempDir()); err == nil {
208 t.Fatal("empty reservation directory established completeness")
209 }
210}
211
212func TestPlan14GenerationSnapshotPermitsFreshTree(t *testing.T) {
213 f := newReuseFixture(t)
214 tree := filepath.Join(f.opt, "versions", "v2")
215 if err := os.RemoveAll(tree); err != nil {
216 t.Fatal(err)
217 }
218 reads := 0
219 source := PinSourceFunc(func(context.Context, string) (StagedPaths, error) { reads++; return nil, os.ErrPermission })
220 f.pass(t, WithRootGenerationPins(source))
221 if got := f.stagedV2(t); got != "new v2" || reads != 0 {
222 t.Fatalf("fresh signed build blocked: %q reads=%d", got, reads)
223 }
224}
225
226func TestPlan14GenerationLegacyPinBootBoundary(t *testing.T) {
227 f := newReuseFixture(t)
228 g, r := generationPinFixture(t, f.opt, "bab", "myapp", "v2")
229 source := RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }}
230 if _, err := source.StagedPins(t.Context(), f.opt); err != nil {
231 t.Fatal("same boot pin refused", err)
232 }
233 r.Binding.Target.BootID = "previous-boot"
234 g.Store = generationListingStore{&healthyRetryStore{dir: t.TempDir()}}
235 if _, err := g.Reserve(r.Binding); err != nil {
236 t.Fatal(err)
237 }
238 if _, err := source.StagedPins(t.Context(), f.opt); err == nil {
239 t.Fatal("old boot pin guessed from device number")
240 }
241}
242