carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostfloor
2
3import (
4 "context"
5 "crypto/ed25519"
6 "crypto/rand"
7 "encoding/json"
8 "errors"
9 "os"
10 "path/filepath"
11 "strings"
12 "syscall"
13 "testing"
14
15 "github.com/carlosframework/platform/internal/release"
16)
17
18type plan9aOfflineStore struct{ release.Store }
19
20func (plan9aOfflineStore) Get(context.Context, string) ([]byte, string, error) {
21 return nil, "", context.DeadlineExceeded
22}
23
24// I-1: an armed box before publication finds neither remote object. Its
25// local policy already enforces the floor, so guard-exec accepts it. A half
26// pair, a malformed object or a different digest still refuses, and only
27// "armed" gets the absent-pair reading.
28func TestPlan9ARemotePolicyArmedBeforePublication(t *testing.T) {
29 pub, key, _ := ed25519.GenerateKey(rand.Reader)
30 sg := release.RootSigner(key)
31 ctx := context.Background()
32 floor := Floor{1, "v1.2.3"}
33 raw, _ := json.Marshal(floor)
34 auth, err := release.SignRootEnvelope(sg, Authorization{1, "moves-protocol", "bucket", Key, Digest(raw)})
35 if err != nil {
36 t.Fatal(err)
37 }
38 local, err := Verify(pub, "bucket", raw, auth)
39 if err != nil {
40 t.Fatal(err)
41 }
42 armed := Policy{1, "bucket", "armed", raw, auth}
43 enforced := Policy{1, "bucket", "enforced", raw, auth}
44
45 empty := release.NewDirStore(t.TempDir())
46 if err := CheckRemotePolicy(ctx, empty, pub, "bucket", armed, local); err != nil {
47 t.Fatalf("armed box refused before publication: %v", err)
48 }
49 if err := CheckRemotePolicy(ctx, empty, pub, "bucket", enforced, local); err == nil {
50 t.Fatal("enforced box accepted a missing remote floor")
51 }
52
53 authOnly := release.NewDirStore(t.TempDir())
54 if _, err := authOnly.Put(ctx, AuthKey, auth, ""); err != nil {
55 t.Fatal(err)
56 }
57 floorOnly := release.NewDirStore(t.TempDir())
58 if _, err := floorOnly.Put(ctx, Key, raw, ""); err != nil {
59 t.Fatal(err)
60 }
61 for name, s := range map[string]release.Store{"auth-only": authOnly, "floor-only": floorOnly} {
62 if err := CheckRemotePolicy(ctx, s, pub, "bucket", armed, local); err == nil {
63 t.Fatalf("%s: half-written pair accepted", name)
64 }
65 }
66
67 malformed := release.NewDirStore(t.TempDir())
68 if _, err := malformed.Put(ctx, AuthKey, auth, ""); err != nil {
69 t.Fatal(err)
70 }
71 if _, err := malformed.Put(ctx, Key, []byte("{"), ""); err != nil {
72 t.Fatal(err)
73 }
74 if err := CheckRemotePolicy(ctx, malformed, pub, "bucket", armed, local); err == nil {
75 t.Fatal("malformed remote floor accepted")
76 }
77
78 other := release.NewDirStore(t.TempDir())
79 if err := Publish(ctx, other, sg, "bucket", Floor{2, "v1.2.4"}); err != nil {
80 t.Fatal(err)
81 }
82 if err := CheckRemotePolicy(ctx, other, pub, "bucket", armed, local); !errors.Is(err, ErrFloor) {
83 t.Fatalf("different remote digest = %v", err)
84 }
85
86 same := release.NewDirStore(t.TempDir())
87 if err := Publish(ctx, same, sg, "bucket", floor); err != nil {
88 t.Fatal(err)
89 }
90 for _, p := range []Policy{armed, enforced} {
91 if err := CheckRemotePolicy(ctx, same, pub, "bucket", p, local); err != nil {
92 t.Fatalf("%s: matching floor refused: %v", p.Mode, err)
93 }
94 }
95 if err := CheckRemotePolicy(ctx, plan9aOfflineStore{}, pub, "bucket", enforced, local); err != nil {
96 t.Fatalf("offline store refused (main accepts it): %v", err)
97 }
98}
99
100type plan9aMixedStore struct {
101 release.Store
102 rawErr, authErr error
103 reads []string
104}
105
106func (s *plan9aMixedStore) Get(_ context.Context, key string) ([]byte, string, error) {
107 s.reads = append(s.reads, key)
108 if key == Key {
109 return nil, "", s.rawErr
110 }
111 return nil, "", s.authErr
112}
113
114func TestPlan9ARemotePolicyMixedErrors(t *testing.T) {
115 for _, mode := range []string{"armed", "enforced"} {
116 for _, tc := range []struct {
117 name string
118 raw, auth, want error
119 reads int
120 }{
121 {"missing-then-offline", os.ErrNotExist, context.DeadlineExceeded, os.ErrNotExist, 1},
122 {"denied-then-offline", os.ErrPermission, context.DeadlineExceeded, os.ErrPermission, 1},
123 {"missing-then-denied", os.ErrNotExist, os.ErrPermission, os.ErrNotExist, 1},
124 {"offline-then-denied", context.DeadlineExceeded, os.ErrPermission, nil, 1},
125 {"ok-then-offline", nil, context.DeadlineExceeded, nil, 2},
126 {"ok-then-denied", nil, os.ErrPermission, os.ErrPermission, 2},
127 {"ok-then-missing", nil, os.ErrNotExist, os.ErrNotExist, 2},
128 } {
129 t.Run(mode+"/"+tc.name, func(t *testing.T) {
130 s := &plan9aMixedStore{rawErr: tc.raw, authErr: tc.auth}
131 err := CheckRemotePolicy(context.Background(), s, nil, "bucket", Policy{Mode: mode}, Verified{})
132 if !errors.Is(err, tc.want) {
133 t.Fatalf("got %v, want %v", err, tc.want)
134 }
135 wantReads := tc.reads
136 if mode == "armed" && errors.Is(tc.raw, os.ErrNotExist) {
137 wantReads = 2
138 }
139 if len(s.reads) != wantReads || s.reads[0] != Key || (wantReads == 2 && s.reads[1] != AuthKey) {
140 t.Fatalf("reads = %v, want %d in Key/AuthKey order", s.reads, wantReads)
141 }
142 })
143 }
144 }
145}
146
147// M1-2: root creates the controller directory the armed path requires,
148// owned by the controllers' account, without following a planted link.
149func TestPlan9AControllerDirCreatedWithoutFollowingLinks(t *testing.T) {
150 uid, gid := os.Getuid(), os.Getgid()
151 check := func(t *testing.T, dir string) {
152 t.Helper()
153 fi, err := os.Lstat(dir)
154 if err != nil {
155 t.Fatal(err)
156 }
157 st := fi.Sys().(*syscall.Stat_t)
158 if !fi.IsDir() || fi.Mode().Perm() != 0o770 || fi.Mode()&os.ModeSetgid == 0 || int(st.Uid) != uid || int(st.Gid) != gid {
159 t.Fatalf("controller dir mode=%v uid=%d gid=%d", fi.Mode(), st.Uid, st.Gid)
160 }
161 }
162 t.Run("absent", func(t *testing.T) {
163 data := t.TempDir()
164 if err := CheckControllersIdle(t.Context(), data); err == nil {
165 t.Fatal("precondition: armed path accepted a missing directory")
166 }
167 if err := EnsureControllerDir(data, uid, gid); err != nil {
168 t.Fatal(err)
169 }
170 check(t, filepath.Join(data, ".hostexec"))
171 if err := (Disk{Participation: floorParticipationFixture(t)}).ControllersIdle(t.Context(), data); err != nil {
172 t.Fatalf("complete root census refused: %v", err)
173 }
174 if err := EnsureControllerDir(data, uid, gid); err != nil {
175 t.Fatalf("second run: %v", err)
176 }
177 })
178 t.Run("existing-loose", func(t *testing.T) {
179 data := t.TempDir()
180 dir := filepath.Join(data, ".hostexec")
181 if err := os.Mkdir(dir, 0o777); err != nil {
182 t.Fatal(err)
183 }
184 if err := os.Chmod(dir, 0o777); err != nil {
185 t.Fatal(err)
186 }
187 if err := os.WriteFile(filepath.Join(dir, "host.json"), []byte("{}"), 0o600); err != nil {
188 t.Fatal(err)
189 }
190 if err := EnsureControllerDir(data, uid, gid); err != nil {
191 t.Fatal(err)
192 }
193 check(t, dir)
194 if _, err := os.Stat(filepath.Join(dir, "host.json")); err != nil {
195 t.Fatal("controller record discarded", err)
196 }
197 })
198 t.Run("symlink", func(t *testing.T) {
199 data, elsewhere := t.TempDir(), t.TempDir()
200 if err := os.Chmod(elsewhere, 0o755); err != nil {
201 t.Fatal(err)
202 }
203 if err := os.Symlink(elsewhere, filepath.Join(data, ".hostexec")); err != nil {
204 t.Skip(err)
205 }
206 if err := EnsureControllerDir(data, uid, gid); err == nil {
207 t.Fatal("followed a planted symlink")
208 }
209 fi, err := os.Stat(elsewhere)
210 if err != nil || fi.Mode().Perm() != 0o755 || fi.Mode()&os.ModeSetgid != 0 {
211 t.Fatalf("link target changed: %v %v", fi.Mode(), err)
212 }
213 })
214 t.Run("file", func(t *testing.T) {
215 data := t.TempDir()
216 if err := os.WriteFile(filepath.Join(data, ".hostexec"), nil, 0o600); err != nil {
217 t.Fatal(err)
218 }
219 if err := EnsureControllerDir(data, uid, gid); err == nil {
220 t.Fatal("accepted a regular file")
221 }
222 })
223 t.Run("data-symlink", func(t *testing.T) {
224 parent := t.TempDir()
225 real := t.TempDir()
226 link := filepath.Join(parent, "data")
227 if err := os.Symlink(real, link); err != nil {
228 t.Skip(err)
229 }
230 if err := EnsureControllerDir(link, uid, gid); err == nil {
231 t.Fatal("followed a symlinked data directory")
232 }
233 if _, err := os.Lstat(filepath.Join(real, ".hostexec")); !errors.Is(err, os.ErrNotExist) {
234 t.Fatalf("created through the link: %v", err)
235 }
236 })
237 t.Run("relative", func(t *testing.T) {
238 if err := EnsureControllerDir("data", uid, gid); err == nil {
239 t.Fatal("accepted a relative data directory")
240 }
241 })
242}
243
244// M1-4: a rollback is recorded in root's own marker before the journal goes,
245// so completion can tell a finished rollback from an update that never
246// changed the box.
247func TestPlan9AMarkerRecordsRollback(t *testing.T) {
248 d := Disk{Dir: t.TempDir(), Participation: floorParticipationFixture(t)}
249 data := t.TempDir()
250 if err := os.Mkdir(filepath.Join(data, ".hostexec"), 0o700); err != nil {
251 t.Fatal(err)
252 }
253 if err := MarkRolledBack(d.Dir, "tx"); !errors.Is(err, os.ErrNotExist) {
254 t.Fatalf("absent marker = %v", err)
255 }
256 if _, err := os.Stat(filepath.Join(d.Dir, "update-begun.json")); !errors.Is(err, os.ErrNotExist) {
257 t.Fatal("marking created a marker", err)
258 }
259 if err := d.Begin(t.Context(), "tx", data); err != nil {
260 t.Fatal(err)
261 }
262 if b, err := ReadBegun(d.Dir); err != nil || b.ID != "tx" || b.RolledBack {
263 t.Fatalf("fresh marker = %+v, %v", b, err)
264 }
265 before, _ := os.ReadFile(filepath.Join(d.Dir, "update-begun.json"))
266 if err := MarkRolledBack(d.Dir, "other"); !errors.Is(err, ErrFloor) {
267 t.Fatalf("foreign marker = %v", err)
268 }
269 if after, _ := os.ReadFile(filepath.Join(d.Dir, "update-begun.json")); string(after) != string(before) {
270 t.Fatal("foreign marking rewrote the marker")
271 }
272 for range 2 {
273 if err := MarkRolledBack(d.Dir, "tx"); err != nil {
274 t.Fatal(err)
275 }
276 }
277 b, err := ReadBegun(d.Dir)
278 if err != nil || b.ID != "tx" || !b.RolledBack {
279 t.Fatalf("marked = %+v, %v", b, err)
280 }
281 raw, _ := os.ReadFile(filepath.Join(d.Dir, "update-begun.json"))
282 if !strings.Contains(string(raw), `"id":"tx"`) {
283 t.Fatalf("marker lost its transaction id: %s", raw)
284 }
285 if err := os.WriteFile(filepath.Join(d.Dir, "update-begun.json"), []byte(`{"id":"tx","rolled_back":"yes"}`), 0o644); err != nil {
286 t.Fatal(err)
287 }
288 if _, err := ReadBegun(d.Dir); err == nil {
289 t.Fatal("malformed marker read")
290 }
291}
292