carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostfiles
2
3import (
4 "context"
5 "crypto/rand"
6 "encoding/hex"
7 "errors"
8 "fmt"
9 "io/fs"
10 "os"
11 "path"
12 "path/filepath"
13 "slices"
14 "sort"
15 "strings"
16
17 "github.com/carlosframework/platform/internal/release"
18)
19
20// syncDir makes a directory's entries durable. A test seam.
21//
22// The open is descriptor-relative (os.Root) with restoreDirFlags:
23// O_DIRECTORY|O_NONBLOCK|O_NOFOLLOW on unix, so whatever was swapped in at
24// rel since realParents looked (a FIFO, a file) is refused at once rather
25// than waited on: a plain open of a FIFO with no writer never returns, and
26// no context can interrupt it.
27var syncDir = func(root *os.Root, rel string) error {
28 d, err := root.OpenFile(filepath.FromSlash(rel), restoreDirFlags, 0)
29 if err != nil {
30 return err
31 }
32 defer d.Close()
33 return d.Sync()
34}
35
36type RestoreStats struct {
37 Written, Removed int
38 Skipped []string // paths the manifest holds as present but unpublished
39 Superseded []string // live paths not written: a live entry above or below them is newer
40}
41
42// Restore writes into dir what the manifest under prefix names and removes
43// what it tombstones: every tombstoned entry is removed, every published one
44// written, and the skipped ones reported. It does not delete files the
45// manifest does not name. It reads nothing but the manifest and the objects
46// the manifest names, and verifies each object against the hash the manifest
47// records.
48//
49// Every path is validated before anything is touched, so a manifest with
50// one bad path changes nothing. Every file operation goes through an
51// os.Root opened on dir, so nothing is written or removed outside it even
52// if dir already holds a symlink pointing elsewhere.
53//
54// When Restore returns nil, what it did is durable: each file was synced
55// before its rename, and every directory in which a name was created,
56// replaced or removed has been synced since. A caller may record the
57// installation as complete on that basis, and on no weaker one.
58//
59// origins are the generation prefixes whose objects this restore may read:
60// the set the caller's pin admits. nil or empty means prefix alone. Every
61// entry's object must be exactly "<origin>objects/<seq>-<sha256>" for one
62// of them, with the entry's own Seq and SHA256; any other entry refuses the
63// whole manifest before anything is touched (ErrOrigin when the key is
64// well formed but its origin is not admitted).
65func Restore(ctx context.Context, s release.Store, prefix, dir string, origins []string) (Manifest, RestoreStats, error) {
66 return restore(ctx, s, prefix, origins, func() (*os.Root, func(), error) {
67 root, err := os.OpenRoot(dir)
68 if err != nil {
69 return nil, nil, fmt.Errorf("hostfiles: restore into %s: %w", dir, err)
70 }
71 return root, func() { root.Close() }, nil
72 })
73}
74
75// RestoreRoot is Restore into a directory the caller has already opened as
76// root, and does not close. os.OpenRoot resolves Restore's dir as a path,
77// following a link anywhere in it and blocking on a FIFO at its last
78// component; a caller restoring into ground someone else can write opens
79// the root itself without either (instancefs.OpenRootBeneath) and restores
80// here. Everything else is Restore's: the one implementation both share.
81func RestoreRoot(ctx context.Context, s release.Store, prefix string, root *os.Root, origins []string) (Manifest, RestoreStats, error) {
82 if root == nil {
83 return Manifest{}, RestoreStats{}, errors.New("hostfiles: restore into a nil root")
84 }
85 return restore(ctx, s, prefix, origins, func() (*os.Root, func(), error) { return root, func() {}, nil })
86}
87
88// restore is Restore and RestoreRoot. open is called only once the
89// manifest is validated and names something to do, so a bad manifest
90// touches no directory and an empty one needs none.
91func restore(ctx context.Context, s release.Store, prefix string, origins []string, open func() (*os.Root, func(), error)) (Manifest, RestoreStats, error) {
92 var st RestoreStats
93 if err := checkPrefix(prefix); err != nil {
94 return Manifest{}, st, err
95 }
96 origins, err := admitted(prefix, origins)
97 if err != nil {
98 return Manifest{}, st, err
99 }
100 m, _, _, err := load(ctx, s, prefix)
101 if err != nil {
102 return Manifest{}, st, err
103 }
104 if err := checkManifest(m, origins); err != nil {
105 return Manifest{}, st, err
106 }
107 var live, dead []string
108 for p, e := range m.Entries {
109 if e.Deleted {
110 dead = append(dead, p)
111 } else {
112 live = append(live, p)
113 }
114 }
115 if len(live)+len(dead) == 0 {
116 return m, st, nil
117 }
118 root, done, err := open()
119 if err != nil {
120 return Manifest{}, st, err
121 }
122 defer done()
123
124 // Tombstones first, deepest first, so a directory that became a file
125 // is emptied before the file is written over it.
126 // Every directory on the way to a path the manifest speaks of, live or
127 // tombstoned, is a directory whose entries this restore may have
128 // changed: now, or in an earlier attempt that removed or created the
129 // name and then failed before its sync. So the set does not depend on
130 // what this call happened to find already done.
131 touched := map[string]bool{".": true}
132 mark := func(p string) {
133 for d := path.Dir(p); d != "."; d = path.Dir(d) {
134 touched[d] = true
135 }
136 }
137 sort.Slice(dead, func(i, j int) bool {
138 di, dj := strings.Count(dead[i], "/"), strings.Count(dead[j], "/")
139 if di != dj {
140 return di > dj
141 }
142 return dead[i] < dead[j]
143 })
144 for _, p := range dead {
145 mark(p)
146 removed, err := removeInside(root, p)
147 if err != nil {
148 return Manifest{}, st, fmt.Errorf("hostfiles: remove %s: %w", p, err)
149 }
150 if removed {
151 st.Removed++
152 }
153 }
154 sort.Strings(live)
155 loser := superseded(m)
156 for _, p := range live {
157 if loser[p] {
158 st.Superseded = append(st.Superseded, p)
159 }
160 }
161 for _, p := range live {
162 if loser[p] {
163 continue
164 }
165 e := m.Entries[p]
166 if e.Skipped != "" {
167 st.Skipped = append(st.Skipped, p)
168 continue
169 }
170 mark(p)
171 body, err := fetch(ctx, s, e, origins)
172 if err != nil {
173 return Manifest{}, st, fmt.Errorf("hostfiles: %s: %w", p, err)
174 }
175 if err := writeInside(root, p, body, os.FileMode(e.Mode&0o777)); err != nil {
176 return Manifest{}, st, fmt.Errorf("hostfiles: write %s: %w", p, err)
177 }
178 st.Written++
179 }
180 // Sync the directories that survive, deepest first and the root last,
181 // so a directory is durable before the entry that names it is. One
182 // that no longer exists as a real directory (it was emptied and then
183 // replaced by a file, or it sat under a path that became a file) has
184 // nothing to sync: the directory above it records its going.
185 dirs := make([]string, 0, len(touched))
186 for d := range touched {
187 dirs = append(dirs, d)
188 }
189 sort.Slice(dirs, func(i, j int) bool {
190 if (dirs[i] == ".") != (dirs[j] == ".") {
191 return dirs[j] == "."
192 }
193 di, dj := strings.Count(dirs[i], "/"), strings.Count(dirs[j], "/")
194 if di != dj {
195 return di > dj
196 }
197 return dirs[i] < dirs[j]
198 })
199 for _, d := range dirs {
200 if d != "." {
201 if ok, err := realParents(root, d+"/x"); err != nil {
202 return Manifest{}, st, fmt.Errorf("hostfiles: sync directory %s: %w", d, err)
203 } else if !ok {
204 continue
205 }
206 }
207 if err := syncDir(root, d); err != nil {
208 return Manifest{}, st, fmt.Errorf("hostfiles: sync directory %s: %w", d, err)
209 }
210 }
211 return m, st, nil
212}
213
214// superseded is the one overlap rule for live entries: a live file and a
215// live entry beneath it cannot both exist, so the newer one (higher Seq)
216// wins and the other is superseded; equal sequences keep the deeper one.
217// Restore leaves a superseded entry unwritten and Present does not look
218// for it, so what Restore produces is exactly what Present accepts
219// (stage B final review A1). Tombstones take no part.
220func superseded(m Manifest) map[string]bool {
221 loser := map[string]bool{}
222 for p, e := range m.Entries {
223 if e.Deleted {
224 continue
225 }
226 for a := path.Dir(p); a != "."; a = path.Dir(a) {
227 above, ok := m.Entries[a]
228 if !ok || above.Deleted {
229 continue
230 }
231 if above.Seq > e.Seq {
232 loser[p] = true
233 } else {
234 loser[a] = true
235 }
236 }
237 }
238 return loser
239}
240
241// ReadPlatform returns a platform-managed item's bytes, verified. A name
242// the manifest does not hold is os.ErrNotExist. prefix is where m was
243// loaded from; origins are as for Restore (nil or empty: prefix alone).
244func ReadPlatform(ctx context.Context, s release.Store, prefix string, m Manifest, name string, origins []string) ([]byte, error) {
245 if err := checkPrefix(prefix); err != nil {
246 return nil, err
247 }
248 origins, err := admitted(prefix, origins)
249 if err != nil {
250 return nil, err
251 }
252 e, ok := m.Platform[name]
253 if !ok {
254 return nil, fmt.Errorf("hostfiles: platform item %q: %w", name, os.ErrNotExist)
255 }
256 return fetch(ctx, s, e, origins)
257}
258
259// checkManifest validates every path and every entry before a caller acts
260// on any of them.
261func checkManifest(m Manifest, origins []string) error {
262 for name, e := range m.Platform {
263 if err := checkEntry(e, origins); err != nil {
264 return fmt.Errorf("hostfiles: platform item %q: %w", name, err)
265 }
266 }
267 for p, e := range m.Entries {
268 if _, err := CleanPath(p); err != nil {
269 return err
270 }
271 if err := withinTreeCap(p); err != nil {
272 return err // no restore could recreate it: refused before anything is touched
273 }
274 if err := checkEntry(e, origins); err != nil {
275 return fmt.Errorf("hostfiles: %s: %w", p, err)
276 }
277 }
278 return nil
279}
280
281// checkEntry refuses a published entry whose object is not one this
282// package would have written for it in an admitted origin, or whose size
283// it could never have published. Tombstones and skipped entries name no
284// object. origins are already validated prefixes.
285func checkEntry(e Entry, origins []string) error {
286 if e.Deleted || e.Skipped != "" {
287 return nil
288 }
289 if len(e.SHA256) != 64 || strings.Trim(e.SHA256, "0123456789abcdef") != "" {
290 return fmt.Errorf("sha256 %q is not 64 lowercase hex characters", e.SHA256)
291 }
292 if _, err := CleanPath(e.Object); err != nil {
293 return fmt.Errorf("object %q is not a clean bucket key", e.Object)
294 }
295 want := objectKey(e.Seq, e.SHA256)
296 origin, ok := strings.CutSuffix(e.Object, want)
297 if !ok {
298 return fmt.Errorf("object %q does not end in %q", e.Object, want)
299 }
300 if !slices.Contains(origins, origin) {
301 return fmt.Errorf("%w: object %q lies under %q, not one of %q", ErrOrigin, e.Object, origin, origins)
302 }
303 if e.Size < 0 || e.Size > MaxFileBytes {
304 return fmt.Errorf("size %d is outside 0 to %d", e.Size, MaxFileBytes)
305 }
306 return nil
307}
308
309// fetch reads e's object at exactly the key the entry names.
310func fetch(ctx context.Context, s release.Store, e Entry, origins []string) ([]byte, error) {
311 if err := checkEntry(e, origins); err != nil {
312 return nil, err
313 }
314 body, _, err := s.Get(ctx, e.Object)
315 if err != nil {
316 return nil, err
317 }
318 if got := sum(body); got != e.SHA256 {
319 return nil, fmt.Errorf("object %s has sha256 %s, the manifest says %s", e.Object, got, e.SHA256)
320 }
321 if int64(len(body)) != e.Size {
322 return nil, fmt.Errorf("object %s has size %d, the manifest says %d", e.Object, len(body), e.Size)
323 }
324 return body, nil
325}
326
327// realParents reports whether every ancestor of rel inside root is a real
328// directory: not a symlink, not a file, not missing.
329func realParents(root *os.Root, rel string) (bool, error) {
330 parts := strings.Split(rel, "/")
331 cur := ""
332 for _, part := range parts[:len(parts)-1] {
333 cur = path.Join(cur, part)
334 fi, err := root.Lstat(filepath.FromSlash(cur))
335 if errors.Is(err, fs.ErrNotExist) {
336 return false, nil
337 }
338 if err != nil {
339 return false, err
340 }
341 if !fi.IsDir() {
342 return false, nil
343 }
344 }
345 return true, nil
346}
347
348// removeInside removes rel if it is a file of ours. A path whose ancestor
349// is a symlink, a file or missing is not ours to remove and is left alone:
350// that is how a tombstone for "a/b" behaves once "a" has become a file,
351// and why a symlinked ancestor never lets a delete reach outside root.
352func removeInside(root *os.Root, rel string) (bool, error) {
353 ok, err := realParents(root, rel)
354 if err != nil || !ok {
355 return false, err
356 }
357 name := filepath.FromSlash(rel)
358 fi, err := root.Lstat(name)
359 if errors.Is(err, fs.ErrNotExist) {
360 return false, nil
361 }
362 if err != nil {
363 return false, err
364 }
365 if fi.IsDir() {
366 // The path is a directory now: a later entry lives under it, and
367 // the tombstone is for a file that no longer exists.
368 return false, nil
369 }
370 if err := root.Remove(name); err != nil && !errors.Is(err, fs.ErrNotExist) {
371 return false, err
372 }
373 return true, nil
374}
375
376// writeInside writes rel atomically inside root. Anything in the way that
377// is not a real directory (a file where a directory must be, a symlink)
378// is removed first, and a directory where the file must be is removed
379// with its contents: the manifest is the truth about this tree.
380func writeInside(root *os.Root, rel string, body []byte, mode os.FileMode) error {
381 if mode == 0 {
382 mode = 0o600
383 }
384 parent := path.Dir(rel)
385 if parent != "." {
386 cur := ""
387 for _, part := range strings.Split(parent, "/") {
388 cur = path.Join(cur, part)
389 fi, err := root.Lstat(filepath.FromSlash(cur))
390 if errors.Is(err, fs.ErrNotExist) {
391 break
392 }
393 if err != nil {
394 return err
395 }
396 if !fi.IsDir() {
397 if err := root.Remove(filepath.FromSlash(cur)); err != nil {
398 return err
399 }
400 break
401 }
402 }
403 if err := root.MkdirAll(filepath.FromSlash(parent), 0o755); err != nil {
404 return err
405 }
406 }
407 name := filepath.FromSlash(rel)
408 if fi, err := root.Lstat(name); err == nil && fi.IsDir() {
409 if err := root.RemoveAll(name); err != nil {
410 return err
411 }
412 }
413 var rnd [8]byte
414 if _, err := rand.Read(rnd[:]); err != nil {
415 return err
416 }
417 tmp := filepath.FromSlash(path.Join(parent, ".carlos-restore-"+hex.EncodeToString(rnd[:])))
418 f, err := root.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
419 if err != nil {
420 return err
421 }
422 done := false
423 defer func() {
424 if !done {
425 f.Close()
426 root.Remove(tmp)
427 }
428 }()
429 if _, err := f.Write(body); err != nil {
430 return err
431 }
432 if err := f.Chmod(mode); err != nil {
433 return err
434 }
435 if err := f.Sync(); err != nil {
436 return err
437 }
438 if err := f.Close(); err != nil {
439 return err
440 }
441 if err := root.Rename(tmp, name); err != nil {
442 return err
443 }
444 done = true
445 return nil
446}
447