carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux && rootrunner
2
3package host
4
5import (
6 "context"
7 "encoding/json"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "testing"
12
13 "github.com/carlosframework/platform/internal/hostbox"
14 "github.com/carlosframework/platform/internal/provision"
15 "github.com/carlosframework/platform/internal/registry"
16 "github.com/carlosframework/platform/internal/release"
17 "github.com/carlosframework/platform/internal/rootstate"
18 "golang.org/x/sys/unix"
19)
20
21func TestPlan14GenerationParkedRootRunner(t *testing.T) {
22 const child = "PLAN14_E38_PARKED_ROOT"
23 dir := os.Getenv(child)
24 if dir == "" {
25 generationRunnerRegistryUser(t)
26 var err error
27 dir, err = os.MkdirTemp("/run", "plan14-parked-")
28 if err != nil {
29 t.Fatal(err)
30 }
31 t.Cleanup(func() {
32 if err := os.RemoveAll(dir); err != nil {
33 t.Error(err)
34 }
35 })
36 if err := os.Chmod(dir, 0755); err != nil {
37 t.Fatal(err)
38 }
39 binary, err := os.Executable()
40 if err != nil {
41 t.Fatal(err)
42 }
43 cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", binary, "-test.run=^TestPlan14GenerationParkedRootRunner$", "-test.v", "-test.timeout=2m")
44 cmd.Dir = dir
45 cmd.Env = append(os.Environ(), child+"="+dir, "TMPDIR="+dir)
46 out, err := cmd.CombinedOutput()
47 if err != nil {
48 t.Fatalf("isolated parked inventory: %v\n%s", err, out)
49 }
50 t.Logf("%s", out)
51 return
52 }
53 if os.Geteuid() != 0 {
54 t.Fatal("designated runner requires root")
55 }
56 if err := unix.Mount("", "/", "", unix.MS_REC|unix.MS_PRIVATE, ""); err != nil {
57 t.Fatal(err)
58 }
59 private := filepath.Join(dir, "varlib")
60 if err := os.Mkdir(private, 0755); err != nil {
61 t.Fatal(err)
62 }
63 if err := unix.Mount(private, "/var/lib", "", unix.MS_BIND, ""); err != nil {
64 t.Fatal(err)
65 }
66 t.Cleanup(func() {
67 if err := unix.Unmount("/var/lib", 0); err != nil {
68 t.Error(err)
69 }
70 })
71 snapshot, j, root := generationParkedFixture(t)
72 if os.Getenv("PLAN14_R11_DELETION") == "1" {
73 j.Target.ClaimETag = "fixture-record"
74 var receipt StopReceipt
75 if err := json.Unmarshal(j.StopReceipt, &receipt); err != nil {
76 t.Fatal(err)
77 }
78 receipt.ClaimETag = j.Target.ClaimETag
79 j.StopReceipt = mustMarshalStopReceipt(receipt)
80 snapshot.Installations[0].Receipts[0].Bytes = j.StopReceipt
81 snapshot.Installations[0].Receipts[0].Target = *j.Target
82 snapshot.Launches[0].Target = *j.Target
83 }
84 t.Setenv("CARLOS_HOST_CLAIM", "true")
85 t.Setenv("CARLOS_OPT_DIR", dir)
86 if err := os.Mkdir(filepath.Join(dir, "versions"), 0755); err != nil {
87 t.Fatal(err)
88 }
89 for _, path := range []string{filepath.Dir(root), root} {
90 if err := os.Chmod(path, 0755); err != nil {
91 t.Fatal(err)
92 }
93 }
94 reg, err := registry.Open(filepath.Join(root, "registry.db"))
95 if err != nil {
96 t.Fatal(err)
97 }
98 owner := registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"}
99 if err := reg.Upsert(registry.Route{Host: j.Inventory.Host, ClaimOwnership: owner}); err != nil {
100 t.Fatal(err)
101 }
102 owner.ClaimRowID = "timer"
103 if err := reg.ReplaceTimers(j.Inventory.Host, []registry.Timer{{Host: j.Inventory.Host, Name: "backup", Kind: "cron", Registration: "timer", ClaimOwnership: owner}}); err != nil {
104 t.Fatal(err)
105 }
106 // The installation, closed launch and stop receipt are fixtures. Parking,
107 // conditional row retirement, checked readback and kernel scanning are real.
108 var deletedStore release.Store
109 if os.Getenv("PLAN14_R11_DELETION") == "1" {
110 deployment := filepath.Join(dir, "deployment")
111 t.Setenv("CARLOS_DEPLOYMENT_DIR", deployment)
112 t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "")
113 deletedStore = release.NewDirStore(deployment)
114 in := snapshot.Installations[0].Installation
115 record := provision.Record{Schema: 1, Host: in.Host, Account: in.Account, App: in.App, Channel: "stable"}
116 if err := provision.CreateRecord(t.Context(), deletedStore, record); err != nil {
117 t.Fatal(err)
118 }
119 if err := deletedStore.Delete(t.Context(), in.Record); err != nil {
120 t.Fatal(err)
121 }
122 }
123 generationRunPark(t, root, j, reg, "")
124 if err := reg.Close(); err != nil {
125 t.Fatal(err)
126 }
127 uid, gid := generationRunnerRegistryUser(t)
128 if err := os.Chown(filepath.Join(root, "registry.db"), uid, gid); err != nil {
129 t.Fatal(err)
130 }
131 if err := os.Chown(root, uid, gid); err != nil {
132 t.Fatal(err)
133 }
134 path := claimRetireJournalDir("", "")
135 state, err := rootstate.Open(path, true, false)
136 if err != nil {
137 t.Fatal(err)
138 }
139 defer state.Close()
140 raw, err := os.ReadFile(filepath.Join(root, "retire", j.ID+".json"))
141 if err != nil {
142 t.Fatal(err)
143 }
144 if err := state.Write(j.ID+".json", raw); err != nil {
145 t.Fatal(err)
146 }
147 if deletedStore != nil {
148 r11DeletedRecordPins(t, snapshot, deletedStore)
149 return
150 }
151 read := func(ctx context.Context) (generationInventory, error) {
152 journals, err := productionGenerationRetirements(ctx)
153 if err != nil {
154 return generationInventory{}, err
155 }
156 return productionGenerationRegistryInventory(ctx, generationParkedSnapshot(t, snapshot, journals))
157 }
158 v := generationReconciler{read: read, kernel: productionGenerationKernelCensus}
159 accept := func() {
160 t.Helper()
161 if _, err := v.reconcile(t.Context(), nil); err != nil {
162 t.Fatal("checked parked reconciliation", err)
163 }
164 }
165 accept()
166 for _, attack := range []string{"uid", "writable-file", "symlink", "writable-parent", "missing-journal", "pending", "parked-replacement"} {
167 t.Run(attack, func(t *testing.T) {
168 file := filepath.Join(path, j.ID+".json")
169 switch attack {
170 case "uid":
171 if err := os.Chown(file, uid, gid); err != nil {
172 t.Fatal(err)
173 }
174 t.Cleanup(func() {
175 if err := os.Chown(file, 0, 0); err != nil {
176 t.Error(err)
177 }
178 })
179 case "writable-file":
180 if err := os.Chmod(file, 0666); err != nil {
181 t.Fatal(err)
182 }
183 t.Cleanup(func() {
184 if err := os.Chmod(file, 0600); err != nil {
185 t.Error(err)
186 }
187 })
188 case "writable-parent":
189 if err := os.Chmod(filepath.Dir(path), 0777); err != nil {
190 t.Fatal(err)
191 }
192 t.Cleanup(func() {
193 if err := os.Chmod(filepath.Dir(path), 0700); err != nil {
194 t.Error(err)
195 }
196 })
197 case "symlink", "missing-journal":
198 if err := os.Rename(file, file+".saved"); err != nil {
199 t.Fatal(err)
200 }
201 if attack == "symlink" {
202 if err := os.Symlink(file+".saved", file); err != nil {
203 t.Fatal(err)
204 }
205 }
206 t.Cleanup(func() {
207 if attack == "symlink" {
208 if err := os.Remove(file); err != nil {
209 t.Error(err)
210 }
211 }
212 if err := os.Rename(file+".saved", file); err != nil {
213 t.Error(err)
214 }
215 })
216 case "pending":
217 var pending hostbox.RetirementJournal
218 if err := json.Unmarshal(raw, &pending); err != nil {
219 t.Fatal(err)
220 }
221 pending.Complete = false
222 body, err := json.Marshal(pending)
223 if err != nil {
224 t.Fatal(err)
225 }
226 if err := state.Write(j.ID+".json", body); err != nil {
227 t.Fatal(err)
228 }
229 t.Cleanup(func() {
230 if err := state.Write(j.ID+".json", raw); err != nil {
231 t.Error(err)
232 }
233 })
234 case "parked-replacement":
235 parked := filepath.Join(root, ".parked", "inc", j.Inventory.Host, j.ID, "data", "instances", j.Inventory.Host)
236 if err := os.Rename(parked, parked+".saved"); err != nil {
237 t.Fatal(err)
238 }
239 if err := os.Mkdir(parked, 0700); err != nil {
240 t.Fatal(err)
241 }
242 t.Cleanup(func() {
243 if err := os.Remove(parked); err != nil {
244 t.Error(err)
245 }
246 if err := os.Rename(parked+".saved", parked); err != nil {
247 t.Error(err)
248 }
249 })
250 }
251 if _, err := v.reconcile(t.Context(), nil); err == nil {
252 t.Fatal("hostile parked state certified")
253 }
254 })
255 accept()
256 }
257}
258