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 "bytes"
7 "context"
8 "database/sql"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "os"
13 "os/exec"
14 "os/signal"
15 "path/filepath"
16 "reflect"
17 "strconv"
18 "strings"
19 "syscall"
20 "testing"
21 "time"
22
23 "github.com/carlosframework/platform/internal/hostbox"
24 "github.com/carlosframework/platform/internal/hostclaim"
25 "github.com/carlosframework/platform/internal/hostguard"
26 "github.com/carlosframework/platform/internal/instancectl"
27)
28
29func TestRootServingParkStoppedWriterHandover(t *testing.T) {
30 runServingParkAuthenticatedPeer(t, true, true)
31}
32
33// Only the fixture unit starts this helper. Keep SQLite open until SIGTERM;
34// the root stop verifier must independently observe descriptor loss.
35func TestRootServingParkWriterPayload(t *testing.T) {
36 path, readyPath := os.Getenv("SERVING_WRITER_DB"), os.Getenv("SERVING_WRITER_READY")
37 if path == "" || readyPath == "" {
38 t.Fatal("invoke through TestRootServingParkStoppedWriterHandover")
39 }
40 stop := make(chan os.Signal, 1)
41 signal.Notify(stop, syscall.SIGTERM)
42 defer signal.Stop(stop)
43 db, err := sql.Open("sqlite", path)
44 if err != nil {
45 t.Fatal(err)
46 }
47 defer db.Close()
48 conn, err := db.Conn(t.Context())
49 if err != nil {
50 t.Fatal(err)
51 }
52 defer conn.Close()
53 for _, query := range []string{"PRAGMA journal_mode=WAL", "CREATE TABLE serving_witness(value TEXT)", "INSERT INTO serving_witness VALUES ('tenant committed bytes')"} {
54 if _, err := conn.ExecContext(t.Context(), query); err != nil {
55 t.Fatal(err)
56 }
57 }
58 start, err := claimConstructionProcessStart()
59 if err != nil {
60 t.Fatal(err)
61 }
62 body, err := json.Marshal(StopPeer{PID: os.Getpid(), ProcessStart: start})
63 if err != nil {
64 t.Fatal(err)
65 }
66 if err := os.WriteFile(readyPath, body, 0600); err != nil {
67 t.Fatal(err)
68 }
69 select {
70 case <-stop:
71 case <-t.Context().Done():
72 t.Fatal(t.Context().Err())
73 }
74}
75
76func runServingPeerStoppedWriter(t *testing.T, f *servingRootFixture, binary string, launch hostbox.LaunchInventory, peer StopPeer, command func(servingPeerCommand, bool) servingPeerReply, kernelResults <-chan error) {
77 t.Helper()
78 ctx, cancel := context.WithTimeout(t.Context(), 45*time.Second)
79 defer cancel()
80 unitName := instancectl.UnitName(instancectl.RoleRun, f.current.Host)
81 slices := []string{instancectl.SliceName(f.current.Host), "carlos-instances.slice", "carlos.slice"}
82 probe, probeErr := exec.CommandContext(ctx, "systemctl", "show", unitName, "-p", "LoadState", "--value").Output()
83 if strings.TrimSpace(string(probe)) != "not-found" {
84 t.Fatalf("refusing existing/unreadable writer unit: %q %v", probe, probeErr)
85 }
86 for _, name := range slices {
87 out, err := exec.CommandContext(ctx, "systemctl", "show", name, "-p", "ActiveState", "--value").Output()
88 if strings.TrimSpace(string(out)) != "inactive" {
89 t.Fatalf("refusing existing/unreadable fixture unit %s: %q %v", name, out, err)
90 }
91 }
92 executable, err := stopIdentity(binary)
93 if err != nil {
94 t.Fatal(err)
95 }
96 reservation := hostbox.LaunchReservation{ID: "writer:main", Role: "main", InvocationID: "writer", Inputs: "sqlite-witness", Target: launch.Target, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(f.current.Host)}, Executable: hostbox.LaunchFile(executable)}
97 // Fixture unit creation and launch journal writes are explicit seams. The
98 // identity observations, stop verifier and stop transport are production.
99 if err := f.f.journal.IntendUnit(ctx, reservation); err != nil {
100 t.Fatal(err)
101 }
102 readyPath := filepath.Join(filepath.Dir(binary), "writer-ready")
103 content := fmt.Sprintf("[Unit]\nDescription=Serving park SQLite witness\n[Service]\nType=simple\nUser=root\nSlice=%s\nExecStart=%s -test.run=^TestRootServingParkWriterPayload$ -test.v -test.timeout=60s\nEnvironment=SERVING_WRITER_DB=%s SERVING_WRITER_READY=%s\nIPAddressDeny=any\nTimeoutStopSec=10s\n", slices[0], binary, f.db, readyPath)
104 unitPath := filepath.Join("/run/systemd/system", unitName)
105 unit, err := os.OpenFile(unitPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
106 if err != nil {
107 t.Fatal(err)
108 }
109 t.Cleanup(func() {
110 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
111 defer cancel()
112 for _, name := range append([]string{unitName}, slices...) {
113 if out, err := exec.CommandContext(ctx, "systemctl", "stop", name).CombinedOutput(); err != nil {
114 t.Errorf("stop fixture %s: %v %s", name, err, out)
115 }
116 }
117 if err := os.Remove(unitPath); err != nil {
118 t.Error(err)
119 }
120 if out, err := exec.CommandContext(ctx, "systemctl", "daemon-reload").CombinedOutput(); err != nil {
121 t.Errorf("reload fixture: %v %s", err, out)
122 }
123 })
124 _, writeErr := unit.WriteString(content)
125 if err := errors.Join(writeErr, unit.Close()); err != nil {
126 t.Fatal(err)
127 }
128 servingRunnerSystemctl(t, "daemon-reload")
129 servingRunnerSystemctl(t, "start", unitName)
130 var process StopPeer
131 for {
132 body, err := os.ReadFile(readyPath)
133 if err == nil && json.Unmarshal(body, &process) == nil {
134 break
135 }
136 select {
137 case <-ctx.Done():
138 t.Fatal("writer readiness", ctx.Err())
139 case <-time.After(20 * time.Millisecond):
140 }
141 }
142 properties := func(name string) string {
143 t.Helper()
144 out, err := exec.CommandContext(ctx, "systemctl", "show", unitName, "-p", name, "--value").Output()
145 if err != nil {
146 t.Fatal(err)
147 }
148 return strings.TrimSpace(string(out))
149 }
150 pid, err := strconv.Atoi(properties("MainPID"))
151 if err != nil || pid != process.PID || pid <= 0 {
152 t.Fatal("writer MainPID", pid, process.PID, err)
153 }
154 start, err := claimLaunchProcessStart("/proc", pid)
155 if err != nil || process.ProcessStart != start {
156 t.Fatalf("writer identity: %+v %v", process, err)
157 }
158 group := reservation.Group
159 group.InvocationID = properties("InvocationID")
160 if properties("ControlGroup") != "/"+group.RelativePath || group.InvocationID == "" {
161 t.Fatal("writer unit identity")
162 }
163 groupID, err := stopIdentity(filepath.Join("/sys/fs/cgroup", group.RelativePath))
164 if err != nil {
165 t.Fatal(err)
166 }
167 group.Device, group.Inode = groupID.Device, groupID.Inode
168 if err := f.f.journal.StartedUnit(ctx, reservation, group, pid, start); err != nil {
169 t.Fatal(err)
170 }
171 id, err := stopIdentity(f.db)
172 if err != nil {
173 t.Fatal(err)
174 }
175 if err := f.f.journal.RecordFile(ctx, launch.Target, hostbox.LaunchFile(id)); err != nil {
176 t.Fatal(err)
177 }
178 if err := os.WriteFile(f.db+".gen", []byte(f.current.Generation), 0600); err != nil {
179 t.Fatal(err)
180 }
181 current := f.f.read()
182 req := StopRequest{Region: f.actor.Region, Host: current.Host, Incarnation: f.current.Incarnation, InstallationStamp: f.current.Stamp, WriterSession: f.current.Session, Box: f.actor.Box, Enrollment: f.actor.Enrollment, BootID: peer.BootID, ControllerID: peer.ControllerID, ExpectedClaimETag: current.ETag, ExpectedDirectoryRevision: "7", RosterRevision: 7}
183 stop := servingPeerCommand{Action: "stop", Stop: req}
184 checkNoReceipt := func() {
185 t.Helper()
186 receipts, err := f.f.guard.CompletedServingStops(ctx, current.Host, hostguard.Captured{Incarnation: f.current.Incarnation, Stamp: f.current.Stamp}, f.current.Session, f.actor.Box)
187 if !errors.Is(err, hostguard.ErrNoFenceReceipt) || len(receipts) != 0 {
188 t.Fatal("root trusted a drain acknowledgement while a writer remained")
189 }
190 }
191 command(stop, true)
192 select {
193 case err := <-kernelResults:
194 if err == nil {
195 t.Fatal("live writer escaped kernel verification")
196 }
197 default:
198 t.Fatal("stop refused before reaching the production kernel verifier")
199 }
200 checkNoReceipt()
201 servingRunnerSystemctl(t, "stop", unitName)
202 if properties("MainPID") != "0" || properties("ActiveState") != "inactive" || properties("Result") != "success" {
203 t.Fatal("writer unit did not stop cleanly")
204 }
205 if _, err := claimLaunchProcessStart("/proc", pid); !errors.Is(err, os.ErrNotExist) {
206 t.Fatalf("writer process still observable: %v", err)
207 }
208 if err := f.f.journal.SettleUnit(ctx, launch.Target, reservation.ID); err != nil {
209 t.Fatal(err)
210 }
211 // Even an empty group and settled journal cannot hide an independent FD.
212 openDB, err := os.Open(f.db)
213 if err != nil {
214 t.Fatal(err)
215 }
216 defer openDB.Close()
217 command(stop, true)
218 select {
219 case err := <-kernelResults:
220 if err == nil || !strings.Contains(err.Error(), "descriptor remains") {
221 t.Fatalf("independent FD refusal: %v", err)
222 }
223 default:
224 t.Fatal("FD refusal did not reach the production kernel verifier")
225 }
226 checkNoReceipt()
227 if err := openDB.Close(); err != nil {
228 t.Fatal(err)
229 }
230 stopped := command(stop, false).Stopped
231 select {
232 case err := <-kernelResults:
233 if err != nil {
234 t.Fatal("stopped writer kernel verification", err)
235 }
236 default:
237 t.Fatal("stop succeeded without production kernel verification")
238 }
239 if err := verifyRetirementReceipt(ctx, hostguard.FenceReceipt{Target: stopped.Target, Bytes: stopped.Bytes, EvidenceAt: stopped.EvidenceAt}); err != nil {
240 t.Fatal("independent stop receipt", err)
241 }
242 if replay := command(stop, false).Stopped; !reflect.DeepEqual(replay, stopped) {
243 t.Fatal("stop replay changed receipt")
244 }
245 before, err := os.ReadFile(f.db)
246 if err != nil {
247 t.Fatal(err)
248 }
249 previous := f.f.read()
250 h := previous.Claim.Holder
251 admitted, err := hostclaim.Update(ctx, f.f.claims, previous, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: f.actor.Box, Region: f.actor.Region, PredecessorRegion: f.actor.Region, Mode: "cold", NewGeneration: "G3"}))
252 if err != nil {
253 t.Fatal(err)
254 }
255 prior := f.current
256 f.current, err = servingWriterIdentity(admitted, f.actor.Box, f.actor.Region)
257 if err != nil {
258 t.Fatal(err)
259 }
260 command(servingPeerCommand{Action: "handover", Previous: previous, Admitted: admitted, Actor: f.actor, Target: stopped.Target}, false)
261 command(servingPeerCommand{Action: "prelaunch", Target: recoveryTuple(f.f.read(), claimStopLocal{Actor: f.actor})}, false)
262 q := servingPeerCommand{Action: "park", Current: f.current, Prior: prior}
263 parked := command(q, false).Authorization
264 f.tags(f.current.Stamp)
265 command(servingPeerCommand{Action: "complete", Current: f.current, ID: parked.ID}, false)
266 record, err := f.p.ledger.read(parked.ID)
267 if err != nil || !record.Complete || record.Disposition != servingParkRetain || len(record.Receipts) != 1 || !bytes.Equal(record.Receipts[0].Bytes, stopped.Bytes) {
268 t.Fatalf("park did not use independent stop: %+v %v", record, err)
269 }
270 parkPath := filepath.Join(f.f.guard.DataDir, "instances", ".serving-parked", parked.ID, filepath.Base(f.db))
271 after, err := os.ReadFile(parkPath)
272 if err != nil || !bytes.Equal(before, after) {
273 t.Fatal("park changed committed SQLite bytes", err)
274 }
275 if _, err := os.Lstat(f.db); !errors.Is(err, os.ErrNotExist) {
276 t.Fatal("park left live database", err)
277 }
278 if replay := command(q, false).Authorization; replay != parked {
279 t.Fatal("stopped-writer park replay changed authority")
280 }
281 for name, want := range map[string]string{"files/upload": "tenant upload", "env": "tenant configuration"} {
282 body, err := os.ReadFile(filepath.Join(filepath.Dir(f.db), name))
283 if err != nil || string(body) != want {
284 t.Fatal("writer retirement changed preserved state", name, err)
285 }
286 }
287 if err := f.p.ledger.fs.CheckParked(&record); err != nil {
288 t.Fatal(err)
289 }
290 servingRestoreAfterStoppedWriter(t, f, servingPeerRetirement{command: command})
291 check, err := f.p.ledger.read(parked.ID)
292 if err != nil || !reflect.DeepEqual(record, check) {
293 t.Fatal("stopped-writer park replay changed record", err)
294 }
295}
296
297// Adapt the fixture's control connection to the production client in the edge
298// process. Root never receives a retirement request from the test controller.
299type servingPeerRetirement struct {
300 command func(servingPeerCommand, bool) servingPeerReply
301}
302
303func (p servingPeerRetirement) Park(ctx context.Context, current, prior servingArtifactIdentity) (servingParkAuthorization, error) {
304 if err := ctx.Err(); err != nil {
305 return servingParkAuthorization{}, err
306 }
307 return p.command(servingPeerCommand{Action: "park", Current: current, Prior: prior}, false).Authorization, nil
308}
309
310func (p servingPeerRetirement) Complete(ctx context.Context, id string, current servingArtifactIdentity) error {
311 if err := ctx.Err(); err != nil {
312 return err
313 }
314 p.command(servingPeerCommand{Action: "complete", Current: current, ID: id}, false)
315 return nil
316}
317
318func (servingPeerRetirement) ParkLegacy(context.Context, servingArtifactIdentity, servingLegacyPredecessor) (servingParkAuthorization, error) {
319 return servingParkAuthorization{}, hostbox.ErrUnsupported
320}
321