Paused: ADR 0325 replaces runs-as and the group grant. Kept for reuse: the bounded descriptor walks, the warrant-backed access act (share, create), the access wait and the missing-path waiting state.
326 lines
10 KiB
Go
326 lines
10 KiB
Go
//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
|
|
}
|