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
2
3package host
4
5import (
6 "bytes"
7 "context"
8 "encoding/json"
9 "github.com/carlosframework/platform/internal/instancectl"
10 "net/http"
11 "net/http/httptest"
12 "os"
13 "path/filepath"
14 "testing"
15
16 "github.com/carlosframework/platform/internal/cgroup"
17 "github.com/carlosframework/platform/internal/hostbox"
18 "github.com/carlosframework/platform/internal/hostguard"
19)
20
21func TestPlan11Rev4JournalRefusesForeignContainment(t *testing.T) {
22 oldRoot, oldInspect := claimContainmentAgentRoot, claimContainmentInspect
23 t.Cleanup(func() { claimContainmentAgentRoot, claimContainmentInspect = oldRoot, oldInspect })
24 claimContainmentAgentRoot = func(context.Context) (string, error) { return "/sys/fs/cgroup/system.slice/carlos-edge.service", nil }
25 claimContainmentInspect = func(p string) (hostbox.LaunchGroup, error) {
26 return hostbox.LaunchGroup{RelativePath: p, Device: 1, Inode: 2}, nil
27 }
28
29 for _, op := range []string{"create", "prelaunch", "reserve"} {
30 for _, path := range []string{"system.slice/ssh.service", "system.slice/carlos-edge.service/carlos-exec@other.example", "derived"} {
31
32 t.Run(op+"/"+path, func(t *testing.T) {
33 f := newRecoveryFixture(t)
34 f.recordPrelaunch = false
35 c := f.controller("boot-1", "controller-1", 4101, "start-1")
36 admitted := c.admit(t, f)
37 target := recoveryTuple(admitted, c.local)
38 target.Op = admitted.Claim.Op.ID
39 positive := path == "derived" && op != "reserve"
40 if path == "derived" {
41 path = filepath.Join("system.slice/carlos-edge.service", cgroup.ExecName(claimContainmentName(target)))
42 }
43 service, err := hostbox.NewJournalService(f.journal)
44 if err != nil {
45 t.Fatal(err)
46 }
47 root := ClaimRootHTTP{Journals: service}
48 root.Reconcile.Box, root.Reconcile.Enrollment = target.Box, target.Enrollment
49 root.Stop.Coordinator.Evidence = f.claims
50 client, _ := hostbox.RemoteJournals(func(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) {
51 raw, _ := json.Marshal(q)
52 w := httptest.NewRecorder()
53 root.serveJournal(w, httptest.NewRequest(http.MethodPost, "/claim/journal", bytes.NewReader(raw)), f.peer)
54 if w.Code != http.StatusOK {
55 return hostbox.JournalResponse{V: 1, ID: q.ID, Error: "refused"}, nil
56 }
57 var out hostbox.JournalResponse
58 err := json.Unmarshal(w.Body.Bytes(), &out)
59 return out, err
60 })
61 in := hostbox.LaunchInventory{Target: target, ControllerPID: f.peer.PID, ProcessStart: f.peer.ProcessStart, NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: path, Device: 1, Inode: 2}}}
62 switch op {
63 case "create":
64 err = client.Create(t.Context(), in)
65 case "prelaunch":
66 err = client.Prelaunch(t.Context(), in)
67 case "reserve":
68 if err = f.journal.Prelaunch(t.Context(), in); err != nil {
69 t.Fatal(err)
70 }
71 err = client.Reserve(t.Context(), hostbox.LaunchReservation{ID: "x", Role: "main", InvocationID: "x", Inputs: "inputs", Target: target, Group: hostbox.LaunchGroup{RelativePath: path + "/role-x", Device: 1, Inode: 3, InvocationID: "x"}, Executable: hostbox.LaunchFile{Device: 1, Inode: 4}})
72 }
73 if positive {
74 if err != nil {
75 t.Fatalf("valid root intake refused: %v", err)
76 }
77 return
78 }
79 if err == nil {
80 t.Fatal("root accepted unrelated containment or parentless reservation")
81 }
82 })
83 }
84 }
85}
86
87func TestPlan11Rev4RecoveryRefusesForgedPersistedGroups(t *testing.T) {
88 for _, settled := range []bool{false, true} {
89 t.Run(map[bool]string{false: "pending", true: "settled"}[settled], func(t *testing.T) {
90 root, proc := t.TempDir(), t.TempDir()
91 if err := os.MkdirAll(filepath.Join(proc, "sys/kernel/random"), 0700); err != nil {
92 t.Fatal(err)
93 }
94 if err := os.WriteFile(filepath.Join(proc, "sys/kernel/random/boot_id"), []byte("new-boot"), 0600); err != nil {
95 t.Fatal(err)
96 }
97 target := hostguard.FenceTarget{Host: "shop.example", Incarnation: "inc", Stamp: 1, Session: 1, Box: "box", Enrollment: "enroll", BootID: "old-boot", ControllerID: "controller", FenceID: "fence"}
98 j := &rebootJournalFixture{j: hostbox.StopKernelJournal{V: 1, Target: target, ControllerPID: 42, ProcessStart: "42", Settled: settled, Cgroups: []hostbox.StopCgroupIdentity{{RelativePath: "system.slice/ssh.service", Device: 1, Inode: 2}}}}
99 k := LinuxStopKernel{AgentCgroup: func(context.Context) (string, error) { return "/sys/fs/cgroup/system.slice/carlos-edge.service", nil }, Journal: j, CgroupRoot: root, ProcRoot: proc, ManagedRoots: map[string]string{"data": t.TempDir()}}
100 _, _, err := k.recoverAfterReboot(t.Context(), target, StopInventory{Complete: true}, j.j, "new-boot", j)
101 if err == nil {
102 t.Fatal("recovery accepted unrelated persisted containment")
103 }
104 })
105 }
106}
107
108func TestPlan11Rev4ContainmentCapturesOnlyDerivedParent(t *testing.T) {
109 target := hostguard.FenceTarget{Host: "shop.example", Incarnation: "inc", Stamp: 1, Session: 1, Box: "box", Enrollment: "enroll", BootID: "boot", ControllerID: "controller"}
110 parentPath, err := claimContainmentParent("/sys/fs/cgroup/system.slice/carlos-edge.service", target)
111 if err != nil {
112 t.Fatal(err)
113 }
114 parent := hostbox.LaunchGroup{RelativePath: parentPath, Device: 1, Inode: 2}
115 child := hostbox.LaunchGroup{RelativePath: parentPath + "/role-one", Device: 1, Inode: 3, InvocationID: "one"}
116 oldRoot, oldInspect := claimContainmentAgentRoot, claimContainmentInspect
117 t.Cleanup(func() { claimContainmentAgentRoot, claimContainmentInspect = oldRoot, oldInspect })
118 claimContainmentAgentRoot = func(context.Context) (string, error) { return "/sys/fs/cgroup/system.slice/carlos-edge.service", nil }
119 claimContainmentInspect = func(p string) (hostbox.LaunchGroup, error) {
120 if p == parent.RelativePath {
121 return parent, nil
122 }
123 if p == child.RelativePath {
124 g := child
125 g.InvocationID = ""
126 return g, nil
127 }
128 t.Fatalf("root tried to capture foreign containment: %s", p)
129 return hostbox.LaunchGroup{}, nil
130 }
131 j, err := hostbox.OpenLaunchJournal(filepath.Join(t.TempDir(), "launch.db"))
132 if err != nil {
133 t.Fatal(err)
134 }
135 defer j.Close()
136 in := hostbox.LaunchInventory{Target: target, ControllerPID: 42, ProcessStart: "42", NoState: true, Groups: []hostbox.LaunchGroup{parent}}
137 for _, op := range []string{"create", "prelaunch"} {
138 if err := validateClaimJournalContainment(t.Context(), hostbox.JournalRequest{Op: op, Launch: in}, j); err != nil {
139 t.Fatal(err)
140 }
141 for _, foreign := range []string{"system.slice/ssh.service", "system.slice/carlos-edge.service/carlos-exec@other.example"} {
142 forged := in
143 forged.Groups = []hostbox.LaunchGroup{{RelativePath: foreign, Device: 1, Inode: 2}}
144 if err := validateClaimJournalContainment(t.Context(), hostbox.JournalRequest{Op: op, Launch: forged}, j); err == nil {
145 t.Fatal("foreign containment accepted with a valid capture provider")
146 }
147 }
148 forged := in
149 forged.Groups = append([]hostbox.LaunchGroup{}, in.Groups...)
150 forged.Groups[0].Inode++
151 if err := validateClaimJournalContainment(t.Context(), hostbox.JournalRequest{Op: op, Launch: forged}, j); err == nil {
152 t.Fatal("forged kernel identity accepted")
153 }
154 }
155 if err := j.Prelaunch(t.Context(), in); err != nil {
156 t.Fatal(err)
157 }
158 r := hostbox.LaunchReservation{ID: "one", Role: "main", InvocationID: "one", Inputs: "inputs", Target: target, Parent: parent, Group: child, Executable: hostbox.LaunchFile{Device: 1, Inode: 4}}
159 intended := r
160 intended.Group.Device = 0
161 intended.Group.Inode = 0
162 if err := validateClaimJournalContainment(t.Context(), hostbox.JournalRequest{Op: "intend", Reservation: intended}, j); err != nil {
163 t.Fatal(err)
164 }
165 if err := j.IntendRole(t.Context(), intended); err != nil {
166 t.Fatal(err)
167 }
168 if err := validateClaimJournalContainment(t.Context(), hostbox.JournalRequest{Op: "reserve", Reservation: r}, j); err != nil {
169 t.Fatal(err)
170 }
171 parentless := r
172 parentless.Parent = hostbox.LaunchGroup{}
173 if err := validateClaimJournalContainment(t.Context(), hostbox.JournalRequest{Op: "reserve", Reservation: parentless}, j); err == nil {
174 t.Fatal("parentless reservation accepted")
175 }
176 if err := j.Reserve(t.Context(), r); err != nil {
177 t.Fatal(err)
178 }
179 in, err = j.Read(t.Context(), target)
180 if err != nil {
181 t.Fatal(err)
182 }
183 in.Reservations = append(in.Reservations, hostbox.LaunchReservation{Target: target, State: hostbox.UnitIntended, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(target.Host)}})
184 if err := validateClaimContainment(in, parentPath); err != nil {
185 t.Fatal("root's exact isolated unit rejected", err)
186 }
187}
188
189type containmentCountingUnits struct {
190 *launchUnits
191 calls int
192}
193
194func (u *containmentCountingUnits) Gone(ctx context.Context, host, inv string) error {
195 u.calls++
196 return u.launchUnits.Gone(ctx, host, inv)
197}
198
199func TestPlan11Rev4RecoveryValidatesWholeInventoryBeforeEffects(t *testing.T) {
200 for _, kind := range []string{"unrelated-parent", "foreign-unit", "settled-foreign-unit", "parentless-role"} {
201 t.Run(kind, func(t *testing.T) {
202 f := newLaunchFixture(t, servingOp())
203 in, err := f.journal.Read(t.Context(), f.target)
204 if err != nil {
205 t.Fatal(err)
206 }
207 if kind == "unrelated-parent" {
208 in.Groups[0].RelativePath = "system.slice/ssh.service"
209 }
210 j, err := hostbox.OpenLaunchJournal(filepath.Join(t.TempDir(), "forged.db"))
211 if err != nil {
212 t.Fatal(err)
213 }
214 defer j.Close()
215 if err = j.Create(t.Context(), in); err != nil {
216 t.Fatal(err)
217 }
218 r := *f.req.Reservation
219 if kind == "foreign-unit" || kind == "settled-foreign-unit" {
220 r.Group.RelativePath = instancectl.ClaimUnitControlGroup("other.example")
221 }
222 if err = j.IntendUnit(t.Context(), r); err != nil {
223 t.Fatal(err)
224 }
225 if kind == "settled-foreign-unit" {
226 if err = j.SettleUnit(t.Context(), f.target, r.ID); err != nil {
227 t.Fatal(err)
228 }
229 }
230 if kind == "parentless-role" {
231 role := hostbox.LaunchReservation{ID: "role", Role: "writer", InvocationID: "role", Inputs: "inputs", Target: f.target, Group: hostbox.LaunchGroup{RelativePath: in.Groups[0].RelativePath + "/role-x", Device: 1, Inode: 7, InvocationID: "role"}, Executable: hostbox.LaunchFile{Device: 1, Inode: 8}}
232 if err = j.Reserve(t.Context(), role); err != nil {
233 t.Fatal(err)
234 }
235 }
236 units := &containmentCountingUnits{launchUnits: f.units}
237 innerCalls := 0
238 k := claimUnitStopKernel{Journal: j, Units: units, Inner: recoveryKernelFunc(func(context.Context, hostguard.FenceTarget, StopInventory) (StopKernelEvidence, DrainAcknowledgement, error) {
239 innerCalls++
240 return StopKernelEvidence{}, DrainAcknowledgement{}, nil
241 })}
242 if _, _, err = k.RecoverStopped(t.Context(), f.target, StopInventory{}); err == nil {
243 t.Fatal("forged persisted inventory accepted")
244 }
245 if units.calls != 0 || innerCalls != 0 {
246 t.Fatalf("privileged recovery reached before validation: Gone=%d inner=%d", units.calls, innerCalls)
247 }
248 })
249 }
250}
251
252func TestPlan11Rev4LaunchAndStopRefuseForgedContainmentBeforeEffects(t *testing.T) {
253 for _, op := range []string{hostbox.RoleLaunchStart, hostbox.RoleLaunchStop} {
254 t.Run(op, func(t *testing.T) {
255 f := newLaunchFixture(t, servingOp())
256 in, err := f.journal.Read(t.Context(), f.target)
257 if err != nil {
258 t.Fatal(err)
259 }
260 in.Groups[0].RelativePath = "system.slice/ssh.service"
261 j, err := hostbox.OpenLaunchJournal(filepath.Join(t.TempDir(), "forged.db"))
262 if err != nil {
263 t.Fatal(err)
264 }
265 defer j.Close()
266 if err = j.Create(t.Context(), in); err != nil {
267 t.Fatal(err)
268 }
269 f.root.Journal = j
270 units := &containmentCountingUnits{launchUnits: f.units}
271 // The reserved-launch extension is supplied by the original fixture.
272 f.root.Units = units
273 q := f.req
274 if op == hostbox.RoleLaunchStop {
275 q = hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: op, Reservation: f.req.Reservation}
276 }
277 if _, err = f.root.Serve(t.Context(), f.peer, q); err == nil {
278 t.Fatal("forged launch inventory accepted")
279 }
280 starts, stops := f.units.counts()
281 if starts != 0 || stops != 0 || units.calls != 0 {
282 t.Fatalf("privileged actions before validation: starts=%d stops=%d gone=%d", starts, stops, units.calls)
283 }
284 })
285 }
286}
287
288func TestPlan11Rev4StopCannotTombstoneForeignUnit(t *testing.T) {
289 f := newLaunchFixture(t, servingOp())
290 units := &containmentCountingUnits{launchUnits: f.units}
291 f.root.Units = units
292 r := *f.req.Reservation
293 r.ID = "foreign-stop"
294 r.Group.RelativePath = instancectl.ClaimUnitControlGroup("other.example")
295 if _, err := f.root.Serve(t.Context(), f.peer, hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchStop, Reservation: &r}); err == nil {
296 t.Fatal("foreign stop request accepted")
297 }
298 in, err := f.journal.Read(t.Context(), f.target)
299 if err != nil {
300 t.Fatal(err)
301 }
302 starts, stops := f.units.counts()
303 if len(in.Reservations) != 0 || starts != 0 || stops != 0 || units.calls != 0 {
304 t.Fatalf("foreign stop changed state: reservations=%d starts=%d stops=%d gone=%d", len(in.Reservations), starts, stops, units.calls)
305 }
306}
307