carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "os"
8 "path/filepath"
9 "testing"
10
11 "github.com/carlosframework/platform/internal/hostbox"
12 "github.com/carlosframework/platform/internal/hostguard"
13 "github.com/carlosframework/platform/internal/registry"
14 "github.com/carlosframework/platform/internal/release"
15 "github.com/carlosframework/platform/internal/rootparticipation"
16 "github.com/carlosframework/platform/internal/rootstate"
17)
18
19func TestPlan14GenerationRegistryRows(t *testing.T) {
20 for _, attack := range []string{"none", "predecessor", "unknown-route-host", "unknown-timer-host", "unknown-host-during-kernel", "route-missing", "route-incarnation", "route-stamp", "route-owner", "route-id", "timer-missing", "timer-both-omitted", "timer-incarnation", "timer-stamp", "timer-owner", "timer-id", "timer-registration", "timer-extra", "route-entry-missing", "timer-entry-missing", "duplicate-entry", "timer-other-host", "wrong-root", "empty-installation", "unreadable", "changed-during-kernel", "row-next-pass", "timer-delivery-progress", "closed-registry"} {
21 t.Run(attack, func(t *testing.T) {
22 f, disk, _ := generationLaunchFixture(t)
23 if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil {
24 t.Fatal(err)
25 }
26 reservation := readGenerationLaunch(t, disk)
27 views, err := f.root.Guard.Root.GenerationInstallations(t.Context())
28 if err != nil {
29 t.Fatal(err)
30 }
31 launch, err := f.journal.Read(t.Context(), f.target)
32 if err != nil {
33 t.Fatal(err)
34 }
35 if attack == "predecessor" {
36 views[0].Retired = []hostguard.Captured{{Incarnation: f.target.Incarnation, Stamp: f.target.Stamp}}
37 views[0].Installation.Stamp++
38 views[0].Installation.Empty = true
39 }
40 views[0].Installation.Artifacts.Timers = "backup"
41 payloads := generationPayloadFixture(views)
42 index := 0
43 if attack == "predecessor" {
44 index = 1
45 payloads[index].Inventory.Entries = []hostbox.StopInventoryEntry{{Kind: "route", ArtifactID: "route", ManagedRoot: "registry", RelativePath: f.target.Host, RowIdentity: "row", Ownership: "owner", Materialization: "materialized"}}
46 }
47 payloads[index].Inventory.Entries = append(payloads[index].Inventory.Entries, hostbox.StopInventoryEntry{Kind: "timer", ArtifactID: "timer", ManagedRoot: "registry", RelativePath: f.target.Host + "/backup", RowIdentity: "timer", Ownership: "owner", Materialization: "materialized"})
48 snapshot := generationInventory{Census: rootparticipation.Census{Schema: 1, Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}, Installations: views, Payloads: payloads, Launches: []hostbox.LaunchInventory{launch}}
49 registryPath := filepath.Join(t.TempDir(), "registry.db")
50 reg, err := registry.Open(registryPath)
51 if err != nil {
52 t.Fatal(err)
53 }
54 defer reg.Close()
55 t.Setenv("CARLOS_HOST_CLAIM", "true")
56 owner := registry.ClaimOwnership{ClaimIncarnation: f.target.Incarnation, ClaimStamp: f.target.Stamp, ClaimOwner: "owner", ClaimRowID: "row"}
57 route := registry.Route{Host: f.target.Host, ClaimOwnership: owner}
58 timer := registry.Timer{Host: f.target.Host, Name: "backup", Kind: "cron", Registration: "timer", ClaimOwnership: owner}
59 timer.ClaimRowID = "timer"
60 put := func(rt registry.Route, timers []registry.Timer) {
61 t.Helper()
62 if err := reg.Delete(route.Host); err != nil {
63 t.Fatal(err)
64 }
65 if err := reg.ReplaceTimers(route.Host, timers); err != nil {
66 t.Fatal(err)
67 }
68 if rt.Host != "" {
69 if err := reg.Upsert(rt); err != nil {
70 t.Fatal(err)
71 }
72 }
73 }
74 put(route, []registry.Timer{timer})
75 active := false
76 reader := reg
77 v := generationReconciler{read: func(ctx context.Context) (generationInventory, error) {
78 return generationRegistryInventory(ctx, reader, snapshot)
79 }, kernel: func(context.Context) (string, string, error) {
80 if active && attack == "unknown-host-during-kernel" {
81 if err := reg.ReplaceTimers("omitted.example", []registry.Timer{{Host: "omitted.example", Name: "paused", Kind: "cron", NextAt: 0}}); err != nil {
82 t.Fatal(err)
83 }
84 }
85 if active && attack == "changed-during-kernel" {
86 // The registry changes after the first read while root inventory stays fixed.
87 next := route
88 next.ClaimRowID = "replacement"
89 put(next, []registry.Timer{timer})
90 }
91 return "boot", reservation.Binding.Digest, nil
92 }}
93 source := generationPinSource{open: func() (rootstate.Store, error) { return disk, nil }, reconcile: v.reconcile}
94 ledger := &release.RootGenerations{Store: disk}
95 accept := func() {
96 t.Helper()
97 pins, err := source.StagedPins(t.Context(), f.root.generations.optDir)
98 if err != nil {
99 t.Fatal(err)
100 }
101 if pinned, err := pins.Refers(reservation.Binding.Path); err != nil || !pinned {
102 t.Fatalf("reservation lost: %v", err)
103 }
104 if c, err := ledger.Completeness(); err != nil || !c.Complete {
105 t.Fatalf("row census not complete: %+v %v", c, err)
106 }
107 }
108 accept()
109 if attack == "none" || attack == "predecessor" {
110 return
111 }
112 initial, _ := ledger.Completeness()
113 saved, err := json.Marshal(snapshot)
114 if err != nil {
115 t.Fatal(err)
116 }
117 rt, tm := route, timer
118 switch attack {
119 case "unknown-route-host":
120 if err := reg.Upsert(registry.Route{Host: "omitted.example"}); err != nil {
121 t.Fatal(err)
122 }
123 case "unknown-timer-host":
124 if err := reg.ReplaceTimers("omitted.example", []registry.Timer{{Host: "omitted.example", Name: "paused", Kind: "cron", NextAt: 0}}); err != nil {
125 t.Fatal(err)
126 }
127 case "route-missing":
128 rt = registry.Route{}
129 case "route-incarnation":
130 rt.ClaimIncarnation = "other"
131 case "route-stamp":
132 rt.ClaimStamp++
133 case "route-owner":
134 rt.ClaimOwner = "other"
135 case "route-id":
136 rt.ClaimRowID = "other"
137 case "timer-incarnation":
138 tm.ClaimIncarnation = "other"
139 case "timer-stamp":
140 tm.ClaimStamp++
141 case "timer-owner":
142 tm.ClaimOwner = "other"
143 case "timer-id":
144 tm.ClaimRowID = "other"
145 case "timer-registration":
146 tm.Registration = "other"
147 case "route-entry-missing":
148 snapshot.Payloads[index].Inventory.Entries = snapshot.Payloads[index].Inventory.Entries[1:]
149 case "timer-entry-missing", "timer-both-omitted":
150 snapshot.Payloads[index].Inventory.Entries = snapshot.Payloads[index].Inventory.Entries[:1]
151 case "duplicate-entry":
152 snapshot.Payloads[index].Inventory.Entries = append(snapshot.Payloads[index].Inventory.Entries, snapshot.Payloads[index].Inventory.Entries[1])
153 case "timer-other-host":
154 snapshot.Payloads[index].Inventory.Entries[1].RelativePath = "other.example/backup"
155 case "wrong-root":
156 snapshot.Payloads[index].Inventory.Entries[1].ManagedRoot = "data"
157 case "empty-installation":
158 snapshot.Installations[0].Installation.Empty = true
159 snapshot.Payloads[0].Inventory.Entries = nil
160 snapshot.Launches = nil
161 case "unreadable":
162 reader = nil
163 case "row-next-pass":
164 tm.Registration, tm.ClaimRowID = "replacement", "replacement"
165 snapshot.Payloads[index].Inventory.Entries[1].RowIdentity = "replacement"
166 case "timer-delivery-progress":
167 tm.NextAt++
168 tm.Attempts++
169 tm.LastStatus = "running"
170 }
171 timers := []registry.Timer{tm}
172 if attack == "timer-missing" || attack == "timer-both-omitted" {
173 timers = nil
174 }
175 if attack == "timer-extra" {
176 extra := timer
177 extra.Name, extra.Registration, extra.ClaimRowID = "extra", "extra", "extra"
178 timers = append(timers, extra)
179 }
180 put(rt, timers)
181 if attack == "closed-registry" {
182 if err := reg.Close(); err != nil {
183 t.Fatal(err)
184 }
185 }
186 if attack == "row-next-pass" || attack == "timer-delivery-progress" {
187 accept()
188 next, _ := ledger.Completeness()
189 changed := next.Evidence.Installations != initial.Evidence.Installations
190 if changed != (attack == "row-next-pass") {
191 t.Fatalf("row identity digest changed=%v for %s", changed, attack)
192 }
193 return
194 }
195 active = true
196 if pins, err := source.StagedPins(t.Context(), f.root.generations.optDir); err == nil || pins != nil {
197 t.Fatalf("registry mismatch certified: %v %v", pins, err)
198 }
199 if c, err := ledger.Completeness(); err != nil || c.Complete || c.Revision <= initial.Revision {
200 t.Fatalf("invalid rows retained certificate: %+v %v", c, err)
201 }
202 if got, err := ledger.Read(reservation.Binding); err != nil || got.Binding != reservation.Binding || got.Closed {
203 t.Fatal("row refusal changed reservation")
204 }
205 if attack == "closed-registry" {
206 reg, err = registry.Open(registryPath)
207 if err != nil {
208 t.Fatal(err)
209 }
210 defer reg.Close()
211 }
212 active, reader = false, reg
213 if err := reg.Delete("omitted.example"); err != nil {
214 t.Fatal(err)
215 }
216 if err := reg.ReplaceTimers("omitted.example", nil); err != nil {
217 t.Fatal(err)
218 }
219 if err := json.Unmarshal(saved, &snapshot); err != nil {
220 t.Fatal(err)
221 }
222 put(route, []registry.Timer{timer})
223 accept()
224 })
225 }
226}
227
228func TestPlan14GenerationRegistryReadOnly(t *testing.T) {
229 t.Setenv("CARLOS_DATA_DIR", t.TempDir())
230 if _, err := productionGenerationRegistryInventory(t.Context(), generationInventory{}); err == nil {
231 t.Fatalf("missing registry: %v", err)
232 }
233 if _, err := os.Stat(filepath.Join(dataDir(), "registry.db")); !os.IsNotExist(err) {
234 t.Fatalf("read created registry: %v", err)
235 }
236}
237
238func TestPlan14GenerationRegistryProductionReader(t *testing.T) {
239 t.Setenv("CARLOS_DATA_DIR", t.TempDir())
240 path := filepath.Join(dataDir(), "registry.db")
241 reg, err := registry.Open(path)
242 if err != nil {
243 t.Fatal(err)
244 }
245 owner := registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"}
246 if err := reg.Upsert(registry.Route{Host: "tenant.example", ClaimOwnership: owner}); err != nil {
247 t.Fatal(err)
248 }
249 if err := reg.Close(); err != nil {
250 t.Fatal(err)
251 }
252 raw, err := os.ReadFile(path)
253 if err != nil {
254 t.Fatal(err)
255 }
256 snapshot := generationInventory{Payloads: []hostbox.GenerationPayload{{Inventory: hostbox.StopInventory{Host: "tenant.example", Incarnation: "inc", InstallationStamp: 1, Entries: []hostbox.StopInventoryEntry{{Kind: "route", ManagedRoot: "registry", RelativePath: "tenant.example", RowIdentity: "row", Ownership: "owner", Materialization: "materialized"}}}}}}
257 if _, err := productionGenerationRegistryInventory(t.Context(), snapshot); err != nil {
258 t.Fatal(err)
259 }
260 if _, err := productionGenerationRegistryInventory(t.Context(), generationInventory{}); err == nil {
261 t.Fatal("empty root census concealed an unknown registry host")
262 }
263 snapshot.Payloads[0].Inventory.Entries = nil
264 if _, err := productionGenerationRegistryInventory(t.Context(), snapshot); err == nil {
265 t.Fatal("empty retained inventory erased a present row")
266 }
267 after, err := os.ReadFile(path)
268 if err != nil || !bytes.Equal(raw, after) {
269 t.Fatal("read-only source changed registry", err)
270 }
271}
272