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 "context"
7 "errors"
8 "os"
9 "path/filepath"
10 "reflect"
11 "strings"
12 "testing"
13
14 "github.com/carlosframework/platform/internal/hostbox"
15 "golang.org/x/sys/unix"
16)
17
18func servingTopologyFixture(t *testing.T) (*servingRootFixture, servingParkNamespace) {
19 t.Helper()
20 f := newServingRootFixture(t, true)
21 if _, err := f.park(); err != nil {
22 t.Fatal(err)
23 }
24 body, err := f.disk.Read("namespace.json")
25 var n servingParkNamespace
26 if err != nil || servingParkDecode(body, &n) != nil || n.Topology == nil {
27 t.Fatal("persistent topology was not captured", err)
28 }
29 return f, n
30}
31
32func TestServingParkPersistentDirectory(t *testing.T) {
33 d, err := os.Open(t.TempDir())
34 if err != nil {
35 t.Fatal(err)
36 }
37 defer d.Close()
38 if err := d.Chmod(0700); err != nil {
39 t.Fatal(err)
40 }
41 p, err := servingParkPersistentDir(d)
42 if err != nil || !p.valid() {
43 var st unix.Stat_t
44 var fs unix.Statfs_t
45 _ = unix.Fstat(int(d.Fd()), &st)
46 _ = unix.Fstatfs(int(d.Fd()), &fs)
47 h, m, e := unix.NameToHandleAt(int(d.Fd()), "", unix.AT_EMPTY_PATH)
48 id, ie := servingParkDirID(d)
49 t.Fatal("kernel persistent directory", p, err, "mode", st.Mode, "fsid", fs.Fsid, "handle", h.Type(), h.Bytes(), m, e, "statx", id, ie)
50 }
51 other, err := servingParkPersistentDir(d)
52 if err != nil || other != p {
53 t.Fatal("same descriptor changed identity", err)
54 }
55 for _, fault := range []string{"empty", "oversize", "uppercase", "filesystem", "type", "file", "writable"} {
56 t.Run(fault, func(t *testing.T) {
57 bad := p
58 switch fault {
59 case "empty":
60 bad.Handle = ""
61 case "oversize":
62 bad.Handle = strings.Repeat("aa", 129)
63 case "uppercase":
64 bad.Handle = "AA"
65 case "filesystem":
66 bad.Filesystem = [2]int32{}
67 case "type":
68 bad.Type = 0
69 case "file":
70 bad.Mode = unix.S_IFREG | 0700
71 case "writable":
72 bad.Mode |= 0020
73 }
74 if bad.valid() {
75 t.Fatal("invalid persistent identity accepted")
76 }
77 })
78 }
79 if _, err := servingParkPersistentDir(nil); !errors.Is(err, hostbox.ErrUnsupported) {
80 t.Fatal("nil descriptor accepted", err)
81 }
82}
83
84func TestServingParkRebootTopologyReadOnly(t *testing.T) {
85 f, n := servingTopologyFixture(t)
86 fs := f.p.ledger.fs.(*servingParkFS)
87 fs.boot = func() (string, error) { return servingOtherBoot, nil }
88 before := servingBootSnapshot(t, f.disk.dir)
89 claim := f.f.read()
90 var got servingParkRebootObservation
91 err := f.p.ledger.withNamespace(t.Context(), func() error {
92 var err error
93 got, err = fs.inspectRebootTopology(t.Context())
94 return err
95 })
96 if err != nil || got.BootID != servingOtherBoot || !reflect.DeepEqual(got.Previous, n) || got.CurrentParent != n.Parent || got.CurrentRoot != n.Root {
97 t.Fatal("read-only topology observation", got, err)
98 }
99 if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) || !reflect.DeepEqual(claim, f.f.read()) {
100 t.Fatal("inspection changed private authority or claim")
101 }
102 // Recognizing the root grants no authority over its events. The original
103 // namespace still belongs to the old boot, even after successful inspection.
104 if err := fs.recoverRecordedNamespace(t.Context()); err == nil {
105 t.Fatal("observation enabled namespace recovery")
106 }
107 if _, err := f.park(); err == nil {
108 t.Fatal("observation enabled parking")
109 }
110 if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) {
111 t.Fatal("failed recovery rewrote retirement history")
112 }
113}
114
115func TestServingParkRebootTopologyRefusesReplacements(t *testing.T) {
116 for _, fault := range []string{"old-format", "malformed-boot", "same-boot", "parent-boot", "parent-record", "root-record", "filesystem", "parent-handle", "root-handle", "type", "owner", "mode", "root", "parent", "root-symlink", "mount", "read-error", "unsupported", "missing-reader", "quarantine", "corrupt-quarantine", "cancel", "root-race", "parent-race", "boot-race", "mode-race", "cancel-race"} {
117 t.Run(fault, func(t *testing.T) {
118 f, n := servingTopologyFixture(t)
119 fs := f.p.ledger.fs.(*servingParkFS)
120 fs.boot = func() (string, error) { return servingOtherBoot, nil }
121 path := filepath.Join(fs.data, "instances", ".serving-parked")
122 ctx, cancel := context.WithCancel(t.Context())
123 defer cancel()
124 switch fault {
125 case "old-format":
126 n.Topology = nil
127 case "malformed-boot":
128 n.BootID = "invalid"
129 p, err := fs.namespaceParent()
130 if err != nil {
131 t.Fatal(err)
132 }
133 p.BootID = n.BootID
134 if err := f.p.ledger.write("namespace-parent.json", p); err != nil {
135 t.Fatal(err)
136 }
137 case "same-boot":
138 fs.boot = func() (string, error) { return n.BootID, nil }
139 case "parent-boot":
140 n.BootID = "11234567-89ab-4cde-8fab-0123456789ab"
141 case "parent-record":
142 n.Parent.Inode++
143 case "root-record":
144 n.Root.Inode++
145 case "filesystem":
146 n.Topology.Parent.Filesystem[0]++
147 n.Topology.Root.Filesystem = n.Topology.Parent.Filesystem
148 case "parent-handle":
149 n.Topology.Parent.Handle = "aa"
150 case "root-handle":
151 n.Topology.Root.Handle = "aa"
152 case "type":
153 n.Topology.Root.Type++
154 case "owner":
155 n.Topology.Parent.Owner++
156 case "mode":
157 n.Topology.Parent.Mode ^= 0100
158 case "root", "parent", "root-symlink":
159 target := path
160 if fault == "parent" {
161 target = filepath.Dir(path)
162 }
163 if err := os.Rename(target, target+"-old"); err != nil {
164 t.Fatal(err)
165 }
166 if fault == "root-symlink" {
167 if err := os.Symlink(target+"-old", target); err != nil {
168 t.Fatal(err)
169 }
170 } else if err := os.Mkdir(target, 0700); err != nil {
171 t.Fatal(err)
172 }
173 case "mount":
174 fs.mount = func(string) (bool, error) { return false, nil }
175 case "read-error", "unsupported":
176 fs.handle = func(*os.File) (servingParkPersistentDirectory, error) {
177 if fault == "unsupported" {
178 return servingParkPersistentDirectory{}, unix.EOPNOTSUPP
179 }
180 return servingParkPersistentDirectory{}, unix.EIO
181 }
182 case "missing-reader":
183 fs.handle = nil
184 case "quarantine", "corrupt-quarantine":
185 body := []byte("{}")
186 if fault == "corrupt-quarantine" {
187 body = []byte("broken")
188 }
189 if err := f.disk.Write("namespace-quarantine.json", body); err != nil {
190 t.Fatal(err)
191 }
192 case "cancel":
193 cancel()
194 case "root-race", "parent-race", "boot-race", "mode-race", "cancel-race":
195 calls := 0
196 fs.handle = func(d *os.File) (servingParkPersistentDirectory, error) {
197 p, err := servingParkPersistentDir(d)
198 calls++
199 if calls == 2 {
200 if fault == "cancel-race" {
201 cancel()
202 } else if fault == "mode-race" {
203 if err := os.Chmod(filepath.Dir(path), 0777); err != nil {
204 t.Fatal(err)
205 }
206 } else if fault == "boot-race" {
207 fs.boot = func() (string, error) { return n.BootID, nil }
208 } else {
209 target := path
210 if fault == "parent-race" {
211 target = filepath.Dir(path)
212 }
213 if err := os.Rename(target, target+"-old"); err != nil {
214 t.Fatal(err)
215 }
216 if err := os.Mkdir(target, 0700); err != nil {
217 t.Fatal(err)
218 }
219 }
220 }
221 return p, err
222 }
223 }
224 if err := f.p.ledger.write("namespace.json", n); err != nil {
225 t.Fatal(err)
226 }
227 before := servingBootSnapshot(t, f.disk.dir)
228 if got, err := fs.inspectRebootTopology(ctx); err == nil || !reflect.DeepEqual(got, servingParkRebootObservation{}) {
229 t.Fatal("unsafe topology accepted", got, err)
230 }
231 if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) {
232 t.Fatal("refusal changed authority")
233 }
234 })
235 }
236}
237
238func TestServingParkTopologyCaptureFailureRecovery(t *testing.T) {
239 for _, boundary := range []string{"handle", "namespace-before", "namespace-after"} {
240 t.Run(boundary, func(t *testing.T) {
241 f := newServingRootFixture(t, true)
242 fs := f.p.ledger.fs.(*servingParkFS)
243 if boundary == "handle" {
244 fs.handle = func(*os.File) (servingParkPersistentDirectory, error) {
245 return servingParkPersistentDirectory{}, unix.EIO
246 }
247 } else {
248 fail := func(name string, body []byte) error {
249 if name == "namespace.json" {
250 return unix.EIO
251 }
252 return nil
253 }
254 store := quarantineWriteFault{Store: f.disk}
255 if boundary == "namespace-before" {
256 store.before = fail
257 } else {
258 store.after = fail
259 }
260 fs.state = store
261 }
262 if _, err := f.park(); !errors.Is(err, unix.EIO) {
263 t.Fatal("missing capture boundary", err)
264 }
265 if body, err := os.ReadFile(f.db); err != nil || len(body) == 0 {
266 t.Fatal("capture failure changed source", err)
267 }
268 fs.handle, fs.state = servingParkPersistentDir, f.disk
269 if _, err := f.park(); err != nil {
270 t.Fatal("capture recovery", err)
271 }
272 body, err := f.disk.Read("namespace.json")
273 var n servingParkNamespace
274 if err != nil || servingParkDecode(body, &n) != nil || n.Topology == nil {
275 t.Fatal("recovery lost topology", err)
276 }
277 })
278 }
279}
280
281func TestServingParkTopologyNeverUpgradesOldNamespace(t *testing.T) {
282 f, n := servingTopologyFixture(t)
283 fs := f.p.ledger.fs.(*servingParkFS)
284 n.Topology = nil
285 if err := f.p.ledger.write("namespace.json", n); err != nil {
286 t.Fatal(err)
287 }
288 before := servingBootSnapshot(t, f.disk.dir)
289 root, same, err := fs.namespace()
290 if err != nil {
291 t.Fatal("same-boot old namespace became unreadable", err)
292 }
293 root.Close()
294 if same.Topology != nil {
295 t.Fatal("old namespace gained inferred topology")
296 }
297 if err := fs.recoverRecordedNamespace(t.Context()); err != nil {
298 t.Fatal("same-boot old namespace recovery", err)
299 }
300 fs.boot = func() (string, error) { return servingOtherBoot, nil }
301 if _, err := fs.inspectRebootTopology(t.Context()); err == nil {
302 t.Fatal("old namespace gained cross-boot proof")
303 }
304 if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) {
305 t.Fatal("old namespace was rewritten")
306 }
307}
308