//go:build linux package host import ( "errors" "io" "os" "github.com/carlosframework/platform/internal/hostbox" "golang.org/x/sys/unix" ) const servingParkRootSearchLimit = 128 // The namespace lock covers this lookup. Only the current root or the prior // root in a completed quarantine can contain an authorized event. Quarantine // retention itself never grants access to the obstruction's children. func (f *servingParkFS) eventRoot(r *servingParkRecord) (*os.File, servingParkNamespace, error) { root, n, err := f.namespace() if err != nil { return nil, n, err } if n.Parent != r.Source.Parent { root.Close() return nil, n, hostbox.ErrChanged } if r.Namespace == (servingArtifactDirectory{}) || r.Namespace == n.Root { // An old lost-rmdir reply cannot identify which namespace is absent. // Preserve it until independent evidence resolves that ambiguity. if r.Namespace == (servingArtifactDirectory{}) && r.EventRemoveIntent { body, e := f.state.Read("namespace-quarantine.json") if e == nil { var q servingParkQuarantine if servingParkDecode(body, &q) != nil || q.validate() != nil || q.Previous != nil { root.Close() return nil, n, hostbox.ErrChanged } } else if !errors.Is(e, os.ErrNotExist) { root.Close() return nil, n, e } } return root, n, nil } root.Close() return f.displacedRoot(r) } func (f *servingParkFS) displacedRoot(r *servingParkRecord) (*os.File, servingParkNamespace, error) { var n servingParkNamespace body, err := f.state.Read("namespace-quarantine.json") if err != nil { return nil, n, err } var q servingParkQuarantine 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 { return nil, n, hostbox.ErrChanged } n = *q.Previous parent, id, err := f.parent() if err != nil { return nil, n, err } defer parent.Close() var st unix.Stat_t if err := unix.Fstat(int(parent.Fd()), &st); err != nil || id != n.Parent || st.Mode&0022 != 0 { return nil, n, hostbox.ErrChanged } // Bound both memory and directory work. A larger parent remains blocked; // no partial search is interpreted as absence or permission to replace. names, err := parent.Readdirnames(servingParkRootSearchLimit + 1) if len(names) > servingParkRootSearchLimit || err != nil && !errors.Is(err, io.EOF) { return nil, n, hostbox.ErrChanged } var found *os.File defer func() { if found != nil { found.Close() } }() for _, name := range names { var s unix.Statx_t err := unix.Statx(int(parent.Fd()), name, unix.AT_SYMLINK_NOFOLLOW, unix.STATX_BASIC_STATS|unix.STATX_MNT_ID, &s) if errors.Is(err, unix.ENOENT) { continue } if err != nil { return nil, n, err } candidate := servingArtifactDirectory{Device: unix.Mkdev(s.Dev_major, s.Dev_minor), Inode: s.Ino, Mount: s.Mnt_id} if s.Mask&unix.STATX_MNT_ID == 0 || s.Mode&unix.S_IFMT != unix.S_IFDIR || candidate != n.Root { continue } d, actual, err := f.privateDir(parent, name) if err != nil { return nil, n, err } if actual != n.Root || found != nil { d.Close() return nil, n, hostbox.ErrChanged } found = d } if found == nil { return nil, n, hostbox.ErrChanged } check, current, err := f.parent() if err != nil { return nil, n, err } check.Close() if current != n.Parent { return nil, n, hostbox.ErrChanged } root := found found = nil return root, n, nil } // Upgrade old records only after event identity has been proved. Persist the // root before deletion so an absent event remains unambiguous on replay. func (f *servingParkFS) recordEventRoot(r *servingParkRecord, root *os.File, save func() error) error { id, err := servingParkDirID(root) if err != nil { return err } if r.Namespace != (servingArtifactDirectory{}) { if r.Namespace != id { return hostbox.ErrChanged } return nil } r.Namespace = id return save() }