| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "errors" |
| 7 | "io" |
| 8 | "os" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/hostbox" |
| 11 | "golang.org/x/sys/unix" |
| 12 | ) |
| 13 | |
| 14 | const servingParkRootSearchLimit = 128 |
| 15 | |
| 16 | // The namespace lock covers this lookup. Only the current root or the prior |
| 17 | // root in a completed quarantine can contain an authorized event. Quarantine |
| 18 | // retention itself never grants access to the obstruction's children. |
| 19 | func (f *servingParkFS) eventRoot(r *servingParkRecord) (*os.File, servingParkNamespace, error) { |
| 20 | root, n, err := f.namespace() |
| 21 | if err != nil { |
| 22 | return nil, n, err |
| 23 | } |
| 24 | if n.Parent != r.Source.Parent { |
| 25 | root.Close() |
| 26 | return nil, n, hostbox.ErrChanged |
| 27 | } |
| 28 | if r.Namespace == (servingArtifactDirectory{}) || r.Namespace == n.Root { |
| 29 | // An old lost-rmdir reply cannot identify which namespace is absent. |
| 30 | // Preserve it until independent evidence resolves that ambiguity. |
| 31 | if r.Namespace == (servingArtifactDirectory{}) && r.EventRemoveIntent { |
| 32 | body, e := f.state.Read("namespace-quarantine.json") |
| 33 | if e == nil { |
| 34 | var q servingParkQuarantine |
| 35 | if servingParkDecode(body, &q) != nil || q.validate() != nil || q.Previous != nil { |
| 36 | root.Close() |
| 37 | return nil, n, hostbox.ErrChanged |
| 38 | } |
| 39 | } else if !errors.Is(e, os.ErrNotExist) { |
| 40 | root.Close() |
| 41 | return nil, n, e |
| 42 | } |
| 43 | } |
| 44 | return root, n, nil |
| 45 | } |
| 46 | root.Close() |
| 47 | return f.displacedRoot(r) |
| 48 | } |
| 49 | |
| 50 | func (f *servingParkFS) displacedRoot(r *servingParkRecord) (*os.File, servingParkNamespace, error) { |
| 51 | var n servingParkNamespace |
| 52 | body, err := f.state.Read("namespace-quarantine.json") |
| 53 | if err != nil { |
| 54 | return nil, n, err |
| 55 | } |
| 56 | var q servingParkQuarantine |
| 57 | if servingParkDecode(body, &q) != nil || q.validate() != nil || !q.Complete || q.Previous == nil || q.Parent != r.Source.Parent || r.Namespace != (servingArtifactDirectory{}) && r.Namespace != q.Previous.Root { |
| 58 | return nil, n, hostbox.ErrChanged |
| 59 | } |
| 60 | n = *q.Previous |
| 61 | parent, id, err := f.parent() |
| 62 | if err != nil { |
| 63 | return nil, n, err |
| 64 | } |
| 65 | defer parent.Close() |
| 66 | var st unix.Stat_t |
| 67 | if err := unix.Fstat(int(parent.Fd()), &st); err != nil || id != n.Parent || st.Mode&0022 != 0 { |
| 68 | return nil, n, hostbox.ErrChanged |
| 69 | } |
| 70 | // Bound both memory and directory work. A larger parent remains blocked; |
| 71 | // no partial search is interpreted as absence or permission to replace. |
| 72 | names, err := parent.Readdirnames(servingParkRootSearchLimit + 1) |
| 73 | if len(names) > servingParkRootSearchLimit || err != nil && !errors.Is(err, io.EOF) { |
| 74 | return nil, n, hostbox.ErrChanged |
| 75 | } |
| 76 | var found *os.File |
| 77 | defer func() { |
| 78 | if found != nil { |
| 79 | found.Close() |
| 80 | } |
| 81 | }() |
| 82 | for _, name := range names { |
| 83 | var s unix.Statx_t |
| 84 | err := unix.Statx(int(parent.Fd()), name, unix.AT_SYMLINK_NOFOLLOW, unix.STATX_BASIC_STATS|unix.STATX_MNT_ID, &s) |
| 85 | if errors.Is(err, unix.ENOENT) { |
| 86 | continue |
| 87 | } |
| 88 | if err != nil { |
| 89 | return nil, n, err |
| 90 | } |
| 91 | candidate := servingArtifactDirectory{Device: unix.Mkdev(s.Dev_major, s.Dev_minor), Inode: s.Ino, Mount: s.Mnt_id} |
| 92 | if s.Mask&unix.STATX_MNT_ID == 0 || s.Mode&unix.S_IFMT != unix.S_IFDIR || candidate != n.Root { |
| 93 | continue |
| 94 | } |
| 95 | d, actual, err := f.privateDir(parent, name) |
| 96 | if err != nil { |
| 97 | return nil, n, err |
| 98 | } |
| 99 | if actual != n.Root || found != nil { |
| 100 | d.Close() |
| 101 | return nil, n, hostbox.ErrChanged |
| 102 | } |
| 103 | found = d |
| 104 | } |
| 105 | if found == nil { |
| 106 | return nil, n, hostbox.ErrChanged |
| 107 | } |
| 108 | check, current, err := f.parent() |
| 109 | if err != nil { |
| 110 | return nil, n, err |
| 111 | } |
| 112 | check.Close() |
| 113 | if current != n.Parent { |
| 114 | return nil, n, hostbox.ErrChanged |
| 115 | } |
| 116 | root := found |
| 117 | found = nil |
| 118 | return root, n, nil |
| 119 | } |
| 120 | |
| 121 | // Upgrade old records only after event identity has been proved. Persist the |
| 122 | // root before deletion so an absent event remains unambiguous on replay. |
| 123 | func (f *servingParkFS) recordEventRoot(r *servingParkRecord, root *os.File, save func() error) error { |
| 124 | id, err := servingParkDirID(root) |
| 125 | if err != nil { |
| 126 | return err |
| 127 | } |
| 128 | if r.Namespace != (servingArtifactDirectory{}) { |
| 129 | if r.Namespace != id { |
| 130 | return hostbox.ErrChanged |
| 131 | } |
| 132 | return nil |
| 133 | } |
| 134 | r.Namespace = id |
| 135 | return save() |
| 136 | } |
| 137 | |