package rootstate import ( "context" "errors" "io" "os" "path/filepath" "strings" "time" "github.com/carlosframework/platform/internal/safefile" "golang.org/x/sys/unix" ) type directory struct { file *os.File owner uint32 public bool } // Open validates every ancestor, including /. It never repairs ownership. // Creation is only possible for root, beneath an already checked parent. func Open(path string, create, public bool) (Store, error) { if !filepath.IsAbs(path) || filepath.Clean(path) != path || path == "/" || (create && os.Geteuid() != 0) { return nil, os.ErrPermission } f, err := os.Open("/") if err != nil { return nil, err } defer f.Close() return descend(f, strings.Split(strings.TrimPrefix(path, "/"), "/"), create, public, 0) } func trusted(fd int, owner uint32, kind uint32) error { var st unix.Stat_t if err := unix.Fstat(fd, &st); err != nil { return err } if st.Uid != owner || st.Mode&unix.S_IFMT != kind || st.Mode&0022 != 0 || (kind == unix.S_IFREG && st.Nlink != 1) { return os.ErrPermission } return nil } // The owner argument is private; tests use a real user-owned descriptor. func descend(parent *os.File, parts []string, create, public bool, owner uint32) (Store, error) { return descendSync(parent, parts, create, public, owner, unix.Fsync) } // syncParent is injected per traversal, never through a mutable global. func descendSync(parent *os.File, parts []string, create, public bool, owner uint32, syncParent func(int) error) (Store, error) { fd, err := unix.FcntlInt(parent.Fd(), unix.F_DUPFD_CLOEXEC, 0) if err != nil { return nil, err } defer func() { if fd >= 0 { unix.Close(fd) } }() if err := trusted(fd, owner, unix.S_IFDIR); err != nil { return nil, err } mode := uint32(0700) if public { mode = 0755 } for _, name := range parts { if !component(name) && !(public && !create && component(strings.ReplaceAll(name, "@", "_"))) { return nil, os.ErrPermission } next, e := unix.Openat(fd, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if errors.Is(e, unix.ENOENT) && create { if e = unix.Mkdirat(fd, name, mode); e != nil && !errors.Is(e, unix.EEXIST) { return nil, e } next, e = unix.Openat(fd, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) } if e != nil { return nil, e } if e = trusted(next, owner, unix.S_IFDIR); e != nil { unix.Close(next) return nil, e } var st unix.Stat_t if e = unix.Fstat(next, &st); e != nil || (public && st.Mode&0055 != 0055) { unix.Close(next) if e != nil { return nil, e } return nil, os.ErrPermission } // A prior mkdir may be visible after its parent sync failed. Sync // existing entries too, so retry cannot report an undurable path. if create { if e = syncParent(fd); e != nil { unix.Close(next) return nil, e } } unix.Close(fd) fd = next } var final unix.Stat_t if err := unix.Fstat(fd, &final); err != nil { return nil, err } if !public && final.Mode&0077 != 0 { return nil, os.ErrPermission } f := os.NewFile(uintptr(fd), "rootstate") fd = -1 return &directory{file: f, owner: owner, public: public}, nil } func (d *directory) Close() error { return d.file.Close() } func (d *directory) Sync() error { if err := d.check(); err != nil { return err } return d.file.Sync() } func (d *directory) check() error { return trusted(int(d.file.Fd()), d.owner, unix.S_IFDIR) } func (d *directory) Read(name string) ([]byte, error) { if !component(name) { return nil, os.ErrPermission } if err := d.check(); err != nil { return nil, err } f, err := safefile.OpenFileAt(int(d.file.Fd()), name, os.O_RDONLY, 0) if err != nil { return nil, err } defer f.Close() if err := trusted(int(f.Fd()), d.owner, unix.S_IFREG); err != nil { return nil, err } b, err := io.ReadAll(io.LimitReader(f, MaxRecord+1)) if err == nil && len(b) > MaxRecord { err = os.ErrPermission } return b, err } func (d *directory) Write(name string, body []byte) error { return d.write(name, body, safefile.WriteAt) } func (d *directory) write(name string, body []byte, write func(int, string, []byte, os.FileMode) error) error { if !component(name) || len(body) > MaxRecord || uint32(os.Geteuid()) != d.owner { return os.ErrPermission } if err := d.check(); err != nil { return err } if _, err := d.Read(name); err != nil && !errors.Is(err, os.ErrNotExist) { return err } return write(int(d.file.Fd()), name, body, fileMode(d.public)) } func (d *directory) Names() ([]string, error) { if err := d.check(); err != nil { return nil, err } // A fresh description gives each scan its own directory offset. fd, err := unix.Openat(int(d.file.Fd()), ".", unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) if err != nil { return nil, err } f := os.NewFile(uintptr(fd), "rootstate") defer f.Close() entries, err := f.ReadDir(-1) if err != nil { return nil, err } var names []string for _, entry := range entries { if !component(entry.Name()) { return nil, os.ErrPermission } names = append(names, entry.Name()) } return names, nil } // Lock serializes root readers and writers on an inode the agent cannot replace. func (d *directory) Lock(ctx context.Context, name string) (func(), error) { if ctx == nil || !component(name) || uint32(os.Geteuid()) != d.owner { return nil, os.ErrPermission } if err := ctx.Err(); err != nil { return nil, err } if err := d.check(); err != nil { return nil, err } f, err := safefile.OpenFileAt(int(d.file.Fd()), name, os.O_RDWR|os.O_CREATE, 0600) if err != nil { return nil, err } if err := trusted(int(f.Fd()), d.owner, unix.S_IFREG); err != nil { f.Close() return nil, err } tick := time.NewTicker(10 * time.Millisecond) defer tick.Stop() for { if err := ctx.Err(); err != nil { f.Close() return nil, err } err := unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB) if err == nil { return func() { _ = unix.Flock(int(f.Fd()), unix.LOCK_UN); _ = f.Close() }, nil } if !errors.Is(err, unix.EAGAIN) && !errors.Is(err, unix.EWOULDBLOCK) && !errors.Is(err, unix.EINTR) { f.Close() return nil, err } select { case <-ctx.Done(): f.Close() return nil, ctx.Err() case <-tick.C: } } } func (d *directory) Remove(name string) error { if !component(name) || uint32(os.Geteuid()) != d.owner { return os.ErrPermission } if err := d.check(); err != nil { return err } if _, err := d.Read(name); err != nil && !errors.Is(err, os.ErrNotExist) { return err } if err := unix.Unlinkat(int(d.file.Fd()), name, 0); err != nil && !errors.Is(err, unix.ENOENT) { return err } return unix.Fsync(int(d.file.Fd())) }