~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
222 lines · 4.8 KB Raw History
//go:build linux

package vmruntime

import (
	"encoding/binary"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"syscall"

	"golang.org/x/sys/unix"
)

const (
	aclXattr    = "system.posix_acl_access"
	aclVersion  = 2
	aclUserObj  = 0x01
	aclUser     = 0x02
	aclGroupObj = 0x04
	aclGroup    = 0x08
	aclMask     = 0x10
	aclOther    = 0x20
	aclNoID     = 0xffffffff
)

type aclEntry struct {
	tag  uint16
	perm uint16
	id   uint32
}

type Grant struct {
	Path string `json:"path"`
	UID  int    `json:"uid"`
	Perm uint16 `json:"perm"`
}

func readACL(path string, mode os.FileMode) ([]aclEntry, error) {
	buf := make([]byte, 4096)
	n, err := unix.Getxattr(path, aclXattr, buf)
	if errors.Is(err, unix.ENODATA) {
		m := uint16(mode.Perm())
		return []aclEntry{
			{aclUserObj, (m >> 6) & 7, aclNoID},
			{aclGroupObj, (m >> 3) & 7, aclNoID},
			{aclOther, m & 7, aclNoID},
		}, nil
	}
	if err != nil {
		return nil, err
	}
	if n < 4 || binary.LittleEndian.Uint32(buf) != aclVersion || (n-4)%8 != 0 {
		return nil, fmt.Errorf("vmruntime: %s carries an ACL this runtime cannot read", path)
	}
	var out []aclEntry
	for off := 4; off < n; off += 8 {
		out = append(out, aclEntry{
			tag:  binary.LittleEndian.Uint16(buf[off:]),
			perm: binary.LittleEndian.Uint16(buf[off+2:]),
			id:   binary.LittleEndian.Uint32(buf[off+4:]),
		})
	}
	return out, nil
}

func writeACL(path string, entries []aclEntry) error {
	var mask uint16
	named := false
	var rest []aclEntry
	for _, e := range entries {
		switch e.tag {
		case aclMask:
			continue
		case aclUser, aclGroup:
			named = true
			mask |= e.perm
		case aclGroupObj:
			mask |= e.perm
		}
		rest = append(rest, e)
	}
	if named {
		rest = append(rest, aclEntry{aclMask, mask & 7, aclNoID})
	}
	sort.SliceStable(rest, func(i, j int) bool {
		if rest[i].tag != rest[j].tag {
			return rest[i].tag < rest[j].tag
		}
		return rest[i].id < rest[j].id
	})
	buf := make([]byte, 4+8*len(rest))
	binary.LittleEndian.PutUint32(buf, aclVersion)
	for i, e := range rest {
		off := 4 + 8*i
		binary.LittleEndian.PutUint16(buf[off:], e.tag)
		binary.LittleEndian.PutUint16(buf[off+2:], e.perm)
		binary.LittleEndian.PutUint32(buf[off+4:], e.id)
	}
	return unix.Setxattr(path, aclXattr, buf, 0)
}

func grantUser(path string, uid int, perm uint16) error {
	st, err := os.Stat(path)
	if err != nil {
		return err
	}
	entries, err := readACL(path, st.Mode())
	if err != nil {
		return err
	}
	found := false
	for i, e := range entries {
		if e.tag == aclUser && e.id == uint32(uid) {
			entries[i].perm = perm
			found = true
		}
	}
	if !found {
		entries = append(entries, aclEntry{aclUser, perm, uint32(uid)})
	}
	if err := writeACL(path, entries); err != nil {
		return fmt.Errorf("vmruntime: granting uid %d %o on %s: %w", uid, perm, path, err)
	}
	return nil
}

func revokeUser(path string, uid int) error {
	st, err := os.Stat(path)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err != nil {
		return err
	}
	entries, err := readACL(path, st.Mode())
	if err != nil {
		return err
	}
	var keep []aclEntry
	for _, e := range entries {
		if e.tag == aclUser && e.id == uint32(uid) {
			continue
		}
		keep = append(keep, e)
	}
	if len(keep) == len(entries) {
		return nil
	}
	return writeACL(path, keep)
}

func ownedBy(st os.FileInfo, uid int) bool {
	s, ok := st.Sys().(*syscall.Stat_t)
	return ok && int(s.Uid) == uid
}

type granter struct {
	uid    int
	self   int
	grants []Grant
	seen   map[string]uint16
}

func newGranter(uid int) *granter {
	return &granter{uid: uid, self: os.Getuid(), seen: map[string]uint16{}}
}

func (g *granter) need(path string, perm uint16, record bool) error {
	if have, ok := g.seen[path]; ok && have&perm == perm {
		return nil
	}
	st, err := os.Stat(path)
	if err != nil {
		return err
	}
	other := uint16(st.Mode().Perm()) & 7
	if other&perm == perm {
		g.seen[path] = perm
		return nil
	}
	if !ownedBy(st, g.self) {
		return fmt.Errorf("vmruntime: the VMM's uid %d needs %o on %s, which uid %d does not own and which does not grant it to others; move the state, runtime or asset directory under this account", g.uid, perm, path, g.self)
	}
	if err := grantUser(path, g.uid, perm|g.seen[path]); err != nil {
		return err
	}
	g.seen[path] = perm | g.seen[path]
	if record {
		g.grants = append(g.grants, Grant{Path: path, UID: g.uid, Perm: g.seen[path]})
	}
	return nil
}

func (g *granter) traverse(path string, stopAt string) error {
	dir := filepath.Dir(path)
	for {
		if dir == stopAt {
			return nil
		}
		if err := g.need(dir, 1, true); err != nil {
			return err
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			return nil
		}
		dir = parent
	}
}

func revokeAll(grants []Grant) error {
	var errs []error
	for _, g := range grants {
		if err := revokeUser(g.Path, g.UID); err != nil {
			errs = append(errs, fmt.Errorf("revoking uid %d on %s: %w", g.UID, g.Path, err))
		}
	}
	return errors.Join(errs...)
}