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 "errors"
7 "os"
8 "path/filepath"
9 "reflect"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/rootstate"
14 "golang.org/x/sys/unix"
15)
16
17// Parent authority is a fixture. UID, mount checks, descriptor-relative rename
18// and the private store are production; no agent or running root service is used.
19func TestRootServingParkQuarantine(t *testing.T) {
20 runRootServingParkQuarantine(t, []string{"", "lost-rename-reply", "submount", "symlink"})
21}
22
23func TestRootServingParkUnrecordedReplacement(t *testing.T) {
24 runRootServingParkQuarantine(t, []string{"lost-fresh-record", "lost-retention-reply", "unrecorded-submount", "repeated-full-disk"})
25}
26
27func runRootServingParkQuarantine(t *testing.T, faults []string) {
28 t.Helper()
29 servingRootRunner(t)
30 for _, fault := range faults {
31 t.Run(fault, func(t *testing.T) {
32 root, err := os.MkdirTemp("/var/lib", "carlos-rootrunner-quarantine-")
33 if err != nil {
34 t.Fatal(err)
35 }
36 t.Cleanup(func() {
37 if err := os.RemoveAll(root); err != nil {
38 t.Error(err)
39 }
40 })
41 data := filepath.Join(root, "data")
42 servingRunnerMount(t, data)
43 state, err := rootstate.Open(filepath.Join(root, "authority"), true, false)
44 if err != nil {
45 t.Fatal(err)
46 }
47 t.Cleanup(func() {
48 if err := state.Close(); err != nil {
49 t.Error(err)
50 }
51 })
52 fs := newServingParkFS(data, state)
53 p, parent, err := fs.parent()
54 if err != nil {
55 t.Fatal(err)
56 }
57 p.Close()
58 path := filepath.Join(data, "instances", ".serving-parked")
59 if err := os.Mkdir(path, 0700); err != nil {
60 t.Fatal(err)
61 }
62 if err := os.WriteFile(filepath.Join(path, "unknown"), []byte("retained unknown bytes"), 0600); err != nil {
63 t.Fatal(err)
64 }
65 if fault == "submount" {
66 if err := unix.Mount(path, path, "", unix.MS_BIND, ""); err != nil {
67 t.Fatal(err)
68 }
69 t.Cleanup(func() {
70 if err := unix.Unmount(path, 0); err != nil {
71 t.Error(err)
72 }
73 })
74 }
75 if fault == "symlink" {
76 if err := os.Rename(path, path+"-saved"); err != nil {
77 t.Fatal(err)
78 }
79 if err := os.Symlink(path+"-saved", path); err != nil {
80 t.Fatal(err)
81 }
82 }
83 ledger := servingParkLedger{state: state, fs: fs, now: time.Now}
84 recover := func() (servingParkQuarantine, error) {
85 var q servingParkQuarantine
86 err := ledger.withNamespace(t.Context(), func() error { var err error; q, err = fs.recoverNamespace(t.Context(), parent); return err })
87 return q, err
88 }
89 if fault == "lost-rename-reply" {
90 fs.rename = func(a int, b string, c int, d string, flags uint) error {
91 if err := unix.Renameat2(a, b, c, d, flags); err != nil {
92 return err
93 }
94 return unix.EIO
95 }
96 if _, err := recover(); !errors.Is(err, unix.EIO) {
97 t.Fatal("lost reply", err)
98 }
99 fs.rename = unix.Renameat2
100 }
101 var unrecorded servingArtifactDirectory
102 if fault == "lost-fresh-record" || fault == "lost-retention-reply" || fault == "unrecorded-submount" || fault == "repeated-full-disk" {
103 fs.state = quarantineWriteFault{Store: state, before: func(name string, body []byte) error {
104 var q servingParkQuarantine
105 if name == "namespace-quarantine.json" && servingParkDecode(body, &q) == nil && q.Fresh.Inode != 0 {
106 return unix.ENOSPC
107 }
108 return nil
109 }}
110 if _, err := recover(); !errors.Is(err, unix.ENOSPC) {
111 t.Fatal("missing fresh identity write failure", err)
112 }
113 d, err := os.Open(path)
114 if err != nil {
115 t.Fatal(err)
116 }
117 unrecorded, err = servingParkDirID(d)
118 d.Close()
119 if err != nil {
120 t.Fatal(err)
121 }
122 if err := os.WriteFile(filepath.Join(path, "uncertain"), []byte("unrecorded bytes"), 0600); err != nil {
123 t.Fatal(err)
124 }
125 if fault == "repeated-full-disk" {
126 if _, err := recover(); !errors.Is(err, unix.ENOSPC) {
127 t.Fatal("missing second full disk failure", err)
128 }
129 }
130 fs.state = state
131 if fault == "lost-retention-reply" {
132 fs.rename = func(a int, b string, c int, d string, flags uint) error {
133 if err := unix.Renameat2(a, b, c, d, flags); err != nil {
134 return err
135 }
136 return unix.EIO
137 }
138 if _, err := recover(); !errors.Is(err, unix.EIO) {
139 t.Fatal("missing retention reply failure", err)
140 }
141 }
142 fs = newServingParkFS(data, state)
143 if fault == "unrecorded-submount" {
144 if err := unix.Mount(path, path, "", unix.MS_BIND, ""); err != nil {
145 t.Fatal(err)
146 }
147 t.Cleanup(func() {
148 if err := unix.Unmount(path, 0); err != nil {
149 t.Error(err)
150 }
151 })
152 }
153 }
154 q, err := recover()
155 if fault == "unrecorded-submount" || fault == "repeated-full-disk" {
156 if err == nil {
157 t.Fatal("unrecorded replacement accepted", fault)
158 }
159 if _, err := state.Read("namespace.json"); !errors.Is(err, os.ErrNotExist) {
160 t.Fatal("unsafe namespace published", err)
161 }
162 retained := path
163 if fault == "repeated-full-disk" {
164 retained = filepath.Join(filepath.Dir(path), q.unrecordedName())
165 }
166 body, err := os.ReadFile(filepath.Join(retained, "uncertain"))
167 if err != nil || string(body) != "unrecorded bytes" {
168 t.Fatal("refusal lost unrecorded bytes", err)
169 }
170 return
171 }
172 if fault == "submount" || fault == "symlink" {
173 if err == nil {
174 t.Fatal("unsafe obstruction accepted")
175 }
176 if _, err := state.Read("namespace-quarantine.json"); !errors.Is(err, os.ErrNotExist) {
177 t.Fatal("unsafe quarantine intent", err)
178 }
179 return
180 }
181 if err != nil || !q.Complete {
182 t.Fatalf("quarantine: %+v %v", q, err)
183 }
184 if q.Obstruction.Mount != parent.Mount || q.Fresh.Mount != parent.Mount {
185 t.Fatal("quarantine crossed bind mount")
186 }
187 if unrecorded.Inode != 0 {
188 if q.Unrecorded != unrecorded || !q.UnrecordedMoved || q.Fresh == unrecorded {
189 t.Fatal("unrecorded root became namespace authority")
190 }
191 body, err := os.ReadFile(filepath.Join(filepath.Dir(path), q.unrecordedName(), "uncertain"))
192 if err != nil || string(body) != "unrecorded bytes" {
193 t.Fatal("retention changed unrecorded bytes", err)
194 }
195 }
196 body, err := os.ReadFile(filepath.Join(data, "instances", q.name(), "unknown"))
197 if err != nil || string(body) != "retained unknown bytes" {
198 t.Fatal("quarantine changed bytes", err)
199 }
200 fs = newServingParkFS(data, state)
201 if replay, err := recover(); err != nil || !reflect.DeepEqual(q, replay) {
202 t.Fatal("fresh-adapter replay", err)
203 }
204 r := servingRunnerPark(t, data, state)
205 if err := fs.CheckParked(&r); err != nil {
206 t.Fatal("park after quarantine", err)
207 }
208 body, err = os.ReadFile(filepath.Join(data, "instances", q.name(), "unknown"))
209 if err != nil || string(body) != "retained unknown bytes" {
210 t.Fatal("park touched quarantine", err)
211 }
212 })
213 }
214}
215