//go:build linux package apply // The descriptor half of share.go (novox/hq ADR 0322 §3, §6). import ( "errors" "fmt" "io" "os" "time" "golang.org/x/sys/unix" ) // shareStats is how a walk reads an entry and an open descriptor: the machine's own calls, or a test's stand-in // for a device it cannot mount. type shareStats struct { at func(dirfd int, name string, st *unix.Stat_t) error fd func(fd int, st *unix.Stat_t) error } var machineStats = shareStats{ at: func(dirfd int, name string, st *unix.Stat_t) error { return unix.Fstatat(dirfd, name, st, unix.AT_SYMLINK_NOFOLLOW) }, fd: unix.Fstat, } // SharePath runs the share act on path for gid (ADR 0322 §6): group write on it and on every file and folder // under it, setgid on every folder, nothing else. The path's group must be gid and never a root group's; a // walk stopped at its bound returns what it did with ErrShareBound. func SharePath(path string, gid int, limits ShareLimits) (ShareCounts, error) { return sharePath(path, gid, limits, machineStats) } func sharePath(path string, gid int, limits ShareLimits, stats shareStats) (ShareCounts, error) { w, fd, err := openShared(path, limits, stats) if err != nil { return ShareCounts{}, err } w.change = true var st unix.Stat_t if err := stats.fd(fd, &st); err != nil { unix.Close(fd) return ShareCounts{}, fmt.Errorf("the path cannot be read: %v; nothing was changed", err) } if err := ShareRefusal(int(st.Gid), gid); err != nil { unix.Close(fd) return ShareCounts{}, err } err = w.dir(fd, st) return w.counts, err } // JudgeShared reads whether path is shared (ADR 0322 §3): its group may write it, and it and every folder under // it carry setgid. Reads only, and stops at the first folder without setgid; one met its bound returns // ErrShareBound, and the verdict is then not known. func JudgeShared(path string, limits ShareLimits) (SharedVerdict, error) { return judgeShared(path, limits, machineStats) } func judgeShared(path string, limits ShareLimits, stats shareStats) (SharedVerdict, error) { w, fd, err := openShared(path, limits, stats) if err != nil { return SharedVerdict{}, err } var st unix.Stat_t if err := stats.fd(fd, &st); err != nil { unix.Close(fd) return SharedVerdict{}, err } if st.Mode&0o020 == 0 { unix.Close(fd) return SharedVerdict{Why: "its group cannot write it"}, nil } err = w.dir(fd, st) v := SharedVerdict{Counts: w.counts} switch { case errors.Is(err, errNotShared): v.Why = "a folder in it does not keep its group on new folders (no setgid)" return v, nil case err != nil: return v, err } v.Shared = true return v, nil } // errNotShared stops a judgement at the first folder without setgid. var errNotShared = errors.New("a folder without setgid") // shareWalk is one walk under an access's path, from the descriptor of the path itself. type shareWalk struct { // change is the act; without it the walk reads, and stops at the first folder without setgid. change bool dev uint64 deadline time.Time limit int stats shareStats counts ShareCounts } // openShared opens path as a mesh directory is opened (no link another account could make, the path itself no // link) and starts a walk from it on the path's own device. func openShared(path string, limits ShareLimits, stats shareStats) (*shareWalk, int, error) { if err := shareablePath(path); err != nil { return nil, -1, err } mw := &meshWalk{want: path} fd, err := mw.open() if errors.Is(err, errNotThereYet) { return nil, -1, errors.New("the path is not there; nothing was changed") } if err != nil { return nil, -1, fmt.Errorf("the path is not walked: %v", err) } if mw.At != path { unix.Close(fd) return nil, -1, errors.New("the path is reached through a link, and the share act walks no path through " + "one; nothing was changed") } var st unix.Stat_t if err := stats.fd(fd, &st); err != nil { unix.Close(fd) return nil, -1, fmt.Errorf("the path cannot be read: %v; nothing was changed", err) } return &shareWalk{dev: uint64(st.Dev), deadline: time.Now().Add(limits.Time), limit: limits.Entries, stats: stats}, fd, nil } // read counts one entry, and says the bound when it is met. func (w *shareWalk) read() error { w.counts.Read++ if w.counts.Read > w.limit || time.Now().After(w.deadline) { return ErrShareBound } return nil } // dir is one folder, open on fd, whose stat is st: its own bits, then every entry in it. It closes fd. func (w *shareWalk) dir(fd int, st unix.Stat_t) error { f := os.NewFile(uintptr(fd), "") defer f.Close() if err := w.read(); err != nil { return err } mode := uint32(st.Mode) & 0o7777 want := mode | 0o020 | unix.S_ISGID switch { case !w.change && mode&unix.S_ISGID == 0: return errNotShared case !w.change || want == mode: w.counts.Kept++ default: if err := unix.Fchmod(fd, want); err != nil { return fmt.Errorf("a folder's mode could not be set: %v", err) } w.counts.Folders++ } for { names, err := f.Readdirnames(256) for _, name := range names { if err := w.entry(fd, name); err != nil { return err } } if errors.Is(err, io.EOF) || (err == nil && len(names) == 0) { return nil } if err != nil { return fmt.Errorf("a folder could not be read: %v", err) } } } // entry is one name in the folder open on dirfd: a link passed by, a folder on this device walked, a file // changed through its own descriptor, anything else left. func (w *shareWalk) entry(dirfd int, name string) error { var st unix.Stat_t if err := w.stats.at(dirfd, name, &st); err != nil { if errors.Is(err, unix.ENOENT) { return nil } return fmt.Errorf("an entry could not be read: %v", err) } switch st.Mode & unix.S_IFMT { case unix.S_IFLNK: w.counts.Links++ return nil case unix.S_IFDIR, unix.S_IFREG: default: w.counts.Other++ return nil } if uint64(st.Dev) != w.dev { w.counts.Mounts++ return nil } isDir := st.Mode&unix.S_IFMT == unix.S_IFDIR if !isDir && !w.change { // A judgement reads folders alone: setgid is a folder's. return nil } flags := unix.O_RDONLY | unix.O_NOFOLLOW | unix.O_CLOEXEC | unix.O_NOCTTY | unix.O_NONBLOCK if isDir { flags |= unix.O_DIRECTORY } fd, err := unix.Openat(dirfd, name, flags, 0) switch { case errors.Is(err, unix.ENOENT): return nil case errors.Is(err, unix.ELOOP), errors.Is(err, unix.ENOTDIR): // Swapped for a link, or for something else, after it was looked at: passed by. w.counts.Links++ return nil case err != nil: return fmt.Errorf("an entry could not be opened: %v", err) } var opened unix.Stat_t if err := w.stats.fd(fd, &opened); err != nil { unix.Close(fd) return fmt.Errorf("an entry could not be read: %v", err) } if opened.Dev != st.Dev || opened.Ino != st.Ino || opened.Mode&unix.S_IFMT != st.Mode&unix.S_IFMT { unix.Close(fd) return errors.New("an entry changed while it was walked, and the act stopped there") } if isDir { return w.dir(fd, opened) } defer unix.Close(fd) if err := w.read(); err != nil { return err } mode := uint32(opened.Mode) & 0o7777 if mode&0o020 != 0 { w.counts.Kept++ return nil } if err := unix.Fchmod(fd, mode|0o020); err != nil { return fmt.Errorf("a file's mode could not be set: %v", err) } w.counts.Files++ return nil } // CreatePath runs the create act on a missing accessed path (novox/hq ADR 0324): each missing level made from the // nearest existing ancestor down, each with that ancestor's owner, group and mode, setgid included. No link is // followed (the ancestor is reached as a mesh directory is, and each level is opened with O_NOFOLLOW), no level is // made on another filesystem, no file is made, and nothing that was there is changed: a level that appears while // the act runs stops it. How many levels it made. func CreatePath(path string) (int, error) { return createPath(path, machineStats) } func createPath(path string, stats shareStats) (int, error) { m, err := NearestAncestor(path) if err != nil { return 0, fmt.Errorf("the path cannot be made: %v; nothing was changed", err) } if !m.Info.IsDir() { return 0, errors.New("its nearest existing ancestor is not a folder (a file or a link), and the create act " + "makes a path only below a folder; nothing was changed") } mw := &meshWalk{want: m.Ancestor} fd, err := mw.open() if err != nil { return 0, fmt.Errorf("the nearest existing ancestor is not walked: %v; nothing was changed", err) } if mw.At != m.Ancestor { unix.Close(fd) return 0, errors.New("the nearest existing ancestor is reached through a link, and the create act makes " + "nothing through one; nothing was changed") } var anc unix.Stat_t if err := stats.fd(fd, &anc); err != nil { unix.Close(fd) return 0, fmt.Errorf("the nearest existing ancestor cannot be read: %v; nothing was changed", err) } if err := ShareRefusal(int(anc.Gid), int(anc.Gid)); err != nil { unix.Close(fd) return 0, fmt.Errorf("the create act makes nothing as this ancestor's group: %w", err) } mode := uint32(anc.Mode) & 0o7777 made := 0 for _, name := range m.Levels { // Made with no bits, so it is nobody's until it is the ancestor's. if err := unix.Mkdirat(fd, name, 0); err != nil { unix.Close(fd) if errors.Is(err, unix.EEXIST) { return made, errors.New("a level of the path appeared while the act ran, and the act changes " + "nothing that is there; it stopped there") } return made, fmt.Errorf("a level could not be made: %v", err) } next, err := unix.Openat(fd, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) unix.Close(fd) if err != nil { return made, fmt.Errorf("a level made could not be opened: %v", err) } fd = next made++ var st unix.Stat_t if err := stats.fd(fd, &st); err != nil { unix.Close(fd) return made, fmt.Errorf("a level made could not be read: %v", err) } if uint64(st.Dev) != uint64(anc.Dev) { unix.Close(fd) return made, errors.New("a level made is on another filesystem than its ancestor, and the act stopped there") } if err := unix.Fchown(fd, int(anc.Uid), int(anc.Gid)); err != nil { unix.Close(fd) return made, fmt.Errorf("a level made could not be given its ancestor's owner: %v", err) } if err := unix.Fchmod(fd, mode); err != nil { unix.Close(fd) return made, fmt.Errorf("a level made could not be given its ancestor's mode: %v", err) } } unix.Close(fd) return made, nil }