//go:build !windows package main import ( "context" "errors" "fmt" "os" "os/exec" "os/user" "path/filepath" "strconv" "syscall" ) func faDropSupported() bool { return true } // faRootStateBase: root-owned 0755, so every dropped child can traverse to its // own 0700 leaf. func faRootStateBase() string { return "/var/lib/privatae/freeagent" } // faHandsOverByChown: on unix every file root writes for the child is handed // over by changing its owner. const faHandsOverByChown = true // faResolveDropOwner is the folder's owner on a root install, refused for root / // nobody / unknown; Stat, not Lstat, so a symlinked root resolves to its // target's owner. HOME is the owner's home when that is a directory, else the // (owner-owned) store state dir — never `/`, which is wrong for a Python child // that writes ~/.cache. func faResolveDropOwner(root, stateDir string) (*faOwner, error) { uid, gid, err := faPathOwner(root) if err != nil { return nil, fmt.Errorf("cannot read the folder's owner: %v", err) } if uid == 0 { return nil, errors.New("refusing to run the agent as root: the granted folder is owned by root — grant a folder owned by the user who should read it") } if uid == 65534 { return nil, errors.New("refusing to run the agent as nobody: the granted folder reports owner 65534 (nobody, or an NFS export with root_squash) — a shared account would hold this store's vault") } u, err := user.LookupId(strconv.FormatUint(uint64(uid), 10)) if err != nil { return nil, fmt.Errorf("refusing to run the agent: the granted folder's owner (uid %d) has no account on this machine", uid) } if u.Username == "nobody" { return nil, errors.New("refusing to run the agent as nobody: a shared account would hold this store's vault") } if g, err := strconv.ParseUint(u.Gid, 10, 32); err == nil { gid = uint32(g) // the account's primary group, not the folder's group } // The owner must be able to REACH the folder: a user-owned leaf under a // root-only parent (/srv/data/alice with /srv/data 0700) would otherwise // start two children that exit-loop forever with the card reading // "starting". Checked as the dropped credential will see it: owner, or // group with g+x, or o+x, on every ancestor. if resolved, err := filepath.EvalSymlinks(root); err == nil { for d := filepath.Dir(resolved); d != "/" && d != "."; d = filepath.Dir(d) { if why := faTraversable(d, uid, gid); why != "" { return nil, fmt.Errorf("refusing to run the agent as %s: it cannot reach the granted folder — %s", u.Username, why) } } } home := u.HomeDir if st, err := os.Stat(home); home == "" || err != nil || !st.IsDir() { home = stateDir } return &faOwner{drop: true, uid: uid, gid: gid, name: u.Username, home: home}, nil } // faTraversable: can uid/gid pass through directory d? "" = yes. func faTraversable(d string, uid, gid uint32) string { st, err := os.Stat(d) if err != nil { return d + ": " + err.Error() } duid, dgid, err := faPathOwner(d) if err != nil { return d + ": " + err.Error() } m := st.Mode().Perm() switch { case duid == uid && m&0o100 != 0, dgid == gid && m&0o010 != 0, m&0o001 != 0: return "" } return fmt.Sprintf("%s is %04o, owner uid %d", d, m, duid) } // faHandPath hands the store's state leaf to the owner. Lchown: a link is // re-owned as a link, never followed. func faHandPath(o *faOwner, path string) error { return faChown(path, o.uid, o.gid) } // faCheckOwnedRoot: the directory root is about to write into belongs to the // owner. A directory the child swapped for one of root's would fail this. func faCheckOwnedRoot(r *os.Root, dir string, o *faOwner) error { st, err := r.Stat(".") if err != nil { return err } uid, _, ok := faInfoOwner(st) if !ok || uid != o.uid { return fmt.Errorf("%s is not owned by %s (uid %d) — refusing to write into it", dir, o.name, uid) } return nil } // faSharePub / faShareRuntime: on unix the public cert directory and the // runtime are made readable by their modes (faStoreDirs, faEnsurePyz). func faSharePub(string, *faOwner) error { return nil } func faShareRuntime(string) error { return nil } // faDropEnv is what a dropped child is told about who it is, and which of the // daemon's variables it may keep. It must not inherit root's HOME (/root — // wrong and unwritable after the drop) or root's XDG config dir. func faDropEnv(o *faOwner, stateDir string) (set []string, passthrough []string) { return []string{"HOME=" + o.home, "USER=" + o.name, "LOGNAME=" + o.name}, []string{"PATH", "LANG", "LC_ALL", "TMPDIR", "TEMP", "TMP"} } // faStartChild starts the child. On unix the drop is the credential in // cmd.SysProcAttr, so this is an ordinary start. func faStartChild(_ context.Context, cmd *exec.Cmd, _ *faOwner) (faChild, error) { if err := cmd.Start(); err != nil { return nil, err } return execChild{cmd}, nil } // faPathOwner: uid/gid of the file at path, following symlinks. func faPathOwner(path string) (uint32, uint32, error) { st, err := os.Stat(path) if err != nil { return 0, 0, err } sys, ok := st.Sys().(*syscall.Stat_t) if !ok { return 0, 0, errors.New("no ownership information for this path") } return sys.Uid, sys.Gid, nil } func faChown(path string, uid, gid uint32) error { return os.Lchown(path, int(uid), int(gid)) } // sysProcAttr is the credential the child is started with. Groups nil + // NoSetGroups false = setgroups() with an EMPTY list — the same as // session_unix.go:71, and for the same measured reason: skipping setgroups // leaves the child in root's supplementary groups. func (o *faOwner) sysProcAttr() *syscall.SysProcAttr { if o == nil || !o.drop { return nil } return &syscall.SysProcAttr{Credential: &syscall.Credential{ Uid: o.uid, Gid: o.gid, Groups: nil, NoSetGroups: false}} } // faFdSane: the open log is a plain file with one link. A hard link to a file // root owns would pass the os.Root walk (it is inside the directory) and still // hand the child root's append on the target. func faFdSane(f *os.File) error { st, err := f.Stat() if err != nil { return err } if !st.Mode().IsRegular() { return errors.New("not a regular file") } if sys, ok := st.Sys().(*syscall.Stat_t); ok && sys.Nlink != 1 { return fmt.Errorf("has %d links", sys.Nlink) } return nil } // faInfoOwner: uid/gid from a FileInfo obtained through an os.Root handle. func faInfoOwner(st os.FileInfo) (uint32, uint32, bool) { sys, ok := st.Sys().(*syscall.Stat_t) if !ok { return 0, 0, false } return sys.Uid, sys.Gid, true } // faInfoLinks: the link count from a FileInfo. func faInfoLinks(st os.FileInfo) (uint64, bool) { sys, ok := st.Sys().(*syscall.Stat_t) if !ok { return 0, false } return uint64(sys.Nlink), true }