blob: 8ba2eebc0159c175b30502e0bb65f717c32ceb99 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package suid
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"os"
"os/user"
"strconv"
"strings"
"v.io/v23/verror"
)
const pkgPath = "v.io/x/ref/services/device/internal/suid"
var (
errUserNameMissing = verror.Register(pkgPath+".errUserNameMissing", verror.NoRetry, "{1:}{2:} --username missing{:_}")
errUnknownUser = verror.Register(pkgPath+".errUnknownUser", verror.NoRetry, "{1:}{2:} --username {3}: unknown user{:_}")
errInvalidUID = verror.Register(pkgPath+".errInvalidUID", verror.NoRetry, "{1:}{2:} user.Lookup() returned an invalid uid {3}{:_}")
errInvalidGID = verror.Register(pkgPath+".errInvalidGID", verror.NoRetry, "{1:}{2:} user.Lookup() returned an invalid gid {3}{:_}")
errUIDTooLow = verror.Register(pkgPath+".errUIDTooLow", verror.NoRetry, "{1:}{2:} suidhelper uid {3} is not permitted because it is less than {4}{:_}")
errAtoiFailed = verror.Register(pkgPath+".errAtoiFailed", verror.NoRetry, "{1:}{2:} strconv.Atoi({3}) failed{:_}")
errInvalidFlags = verror.Register(pkgPath+".errInvalidFlags", verror.NoRetry, "{1:}{2:} invalid flags ({3} are set){:_}")
)
type WorkParameters struct {
uid int
gid int
workspace string
agentsock string
logDir string
argv0 string
argv []string
envv []string
dryrun bool
remove bool
chown bool
kill bool
killPids []int
}
type ArgsSavedForTest struct {
Uname string
Workpace string
Run string
LogDir string
}
const SavedArgs = "V23_SAVED_ARGS"
var (
flagUsername, flagWorkspace, flagLogDir, flagRun, flagProgName, flagAgentSock *string
flagMinimumUid *int64
flagRemove, flagKill, flagChown, flagDryrun *bool
)
func init() {
setupFlags(flag.CommandLine)
}
func setupFlags(fs *flag.FlagSet) {
const uidThreshold = 501
flagUsername = fs.String("username", "", "The UNIX user name used for the other functions of this tool.")
flagWorkspace = fs.String("workspace", "", "Path to the application's workspace directory.")
flagAgentSock = fs.String("agentsock", "", "Path to the application's security agent socket.")
flagLogDir = fs.String("logdir", "", "Path to the log directory.")
flagRun = fs.String("run", "", "Path to the application to exec.")
flagProgName = fs.String("progname", "unnamed_app", "Visible name of the application, used in argv[0]")
flagMinimumUid = fs.Int64("minuid", uidThreshold, "UIDs cannot be less than this number.")
flagRemove = fs.Bool("rm", false, "Remove the file trees given as command-line arguments.")
flagKill = fs.Bool("kill", false, "Kill process ids given as command-line arguments.")
flagChown = fs.Bool("chown", false, "Change owner of files and directories given as command-line arguments to the user specified by this flag")
flagDryrun = fs.Bool("dryrun", false, "Elides root-requiring systemcalls.")
}
func cleanEnv(env []string) []string {
nenv := []string{}
for _, e := range env {
if !strings.HasPrefix(e, "V23_SUIDHELPER_TEST") {
nenv = append(nenv, e)
}
}
return nenv
}
// checkFlagCombinations makes sure that a valid combination of flags has been
// specified for rm/kill/chown
//
// --rm and --kill are modal. Complain if any other flag is set along with one of
// those. --chown allows specification of --username, --dryrun, and --minuid,
// but nothing else
func checkFlagCombinations(fs *flag.FlagSet) error {
if !(*flagRemove || *flagKill || *flagChown) {
return nil
}
// Count flags that are set. The device manager test always sets --minuid=1
// and --test.run=TestSuidHelper so when in a test, tolerate those.
flagsToIgnore := map[string]string{}
if os.Getenv("V23_SUIDHELPER_TEST") != "" {
flagsToIgnore["minuid"] = "1"
flagsToIgnore["test.run"] = "TestSuidHelper"
}
if *flagChown {
// Allow all values of --username, --dryrun, and --minuid
flagsToIgnore["username"] = "*"
flagsToIgnore["dryrun"] = "*"
flagsToIgnore["minuid"] = "*"
}
counter := 0
fs.Visit(func(f *flag.Flag) {
if flagsToIgnore[f.Name] != f.Value.String() && flagsToIgnore[f.Name] != "*" {
counter++
}
})
if counter > 1 {
return verror.New(errInvalidFlags, nil, counter, "--rm and --kill cannot be used with any other flag. --chown can only be used with --username, --dryrun, and --minuid")
}
return nil
}
// warnMissingSuidPrivs makes it a little easier to debug when suid privs are required but
// are not present. It's not a comprehensive check -- e.g. we may be running as user
// <username> and suppress the warning, but still fail to chown a file owned by some other user.
func warnMissingSuidPrivs(uid int) {
osUid, osEuid := os.Getuid(), os.Geteuid()
if osUid == 0 || osEuid == 0 || osUid == uid || osEuid == uid {
return
}
fmt.Fprintln(os.Stderr, "uid is ", os.Getuid(), ", effective uid is ", os.Geteuid())
fmt.Fprintln(os.Stderr, "WARNING: suidhelper is not root. Is your filesystem mounted with nosuid?")
}
// ParseArguments populates the WorkParameter object from the provided args
// and env strings.
func (wp *WorkParameters) ProcessArguments(fs *flag.FlagSet, env []string) error {
if err := checkFlagCombinations(fs); err != nil {
return err
}
if *flagRemove {
wp.remove = true
wp.argv = fs.Args()
return nil
}
if *flagKill {
wp.kill = true
for _, p := range fs.Args() {
pid, err := strconv.Atoi(p)
if err != nil {
wp.killPids = nil
return verror.New(errAtoiFailed, nil, p, err)
}
wp.killPids = append(wp.killPids, pid)
}
return nil
}
if *flagDryrun {
wp.uid, wp.gid = -1, -1
} else {
username := *flagUsername
if username == "" {
return verror.New(errUserNameMissing, nil)
}
usr, err := user.Lookup(username)
if err != nil {
return verror.New(errUnknownUser, nil, username)
}
uid, err := strconv.ParseInt(usr.Uid, 0, 32)
if err != nil {
return verror.New(errInvalidUID, nil, usr.Uid)
}
gid, err := strconv.ParseInt(usr.Gid, 0, 32)
if err != nil {
return verror.New(errInvalidGID, nil, usr.Gid)
}
warnMissingSuidPrivs(int(uid))
// Uids less than 501 can be special so we forbid running as them.
if uid < *flagMinimumUid {
return verror.New(errUIDTooLow, nil,
uid, *flagMinimumUid)
}
wp.uid = int(uid)
wp.gid = int(gid)
}
wp.dryrun = *flagDryrun
// At this point, all flags allowed by --chown have been processed
if *flagChown {
wp.chown = true
wp.argv = fs.Args()
return nil
}
// Preserve the arguments for examination by the test harness if executed
// in the course of a test.
if os.Getenv("V23_SUIDHELPER_TEST") != "" {
env = cleanEnv(env)
b := new(bytes.Buffer)
enc := json.NewEncoder(b)
enc.Encode(ArgsSavedForTest{
Uname: *flagUsername,
Workpace: *flagWorkspace,
Run: *flagRun,
LogDir: *flagLogDir,
})
env = append(env, SavedArgs+"="+b.String())
wp.dryrun = true
}
wp.workspace = *flagWorkspace
wp.agentsock = *flagAgentSock
wp.argv0 = *flagRun
wp.logDir = *flagLogDir
wp.argv = append([]string{*flagProgName}, fs.Args()...)
// TODO(rjkroege): Reduce the environment to the absolute minimum needed.
wp.envv = env
return nil
}