| // 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 |
| } |