veyron/services/mgmt/suidhelper: prep for go1.4
Update suidhelper for correct operation on go1.4 and make better use
of Go's syscall interface to reduce likelyhood of flake bugs.
Change-Id: I3f9083a0de5cf020e52f2844f2a28927e15c73ac
diff --git a/services/mgmt/device/impl/app_service.go b/services/mgmt/device/impl/app_service.go
index 940df40..1538c98 100644
--- a/services/mgmt/device/impl/app_service.go
+++ b/services/mgmt/device/impl/app_service.go
@@ -119,7 +119,6 @@
"io/ioutil"
"os"
"os/exec"
- "os/user"
"path"
"path/filepath"
"reflect"
@@ -677,56 +676,6 @@
return instanceDir, instanceID, nil
}
-// isSetuid is defined like this so we can override its
-// implementation for tests.
-var isSetuid = func(fileStat os.FileInfo) bool {
- vlog.VI(2).Infof("running the original isSetuid")
- return fileStat.Mode()&os.ModeSetuid == os.ModeSetuid
-}
-
-// systemAccountForHelper returns the uname that the helper uses to invoke the
-// application. If the helper exists and is setuid, the device manager
-// requires that there is a uname associated with the Veyron
-// identity that requested starting an application.
-// TODO(rjkroege): This function assumes a desktop installation target
-// and is probably not a good fit in other contexts. Revisit the design
-// as appropriate. This function also internalizes a decision as to when
-// it is possible to start an application that needs to be made explicit.
-func systemAccountForHelper(ctx ipc.ServerContext, helperPath string, uat BlessingSystemAssociationStore) (systemName string, err error) {
- identityNames := ctx.RemoteBlessings().ForContext(ctx)
- helperStat, err := os.Stat(helperPath)
- if err != nil {
- vlog.Errorf("Stat(%v) failed: %v. helper is required.", helperPath, err)
- return "", verror2.Make(ErrOperationFailed, ctx.Context())
- }
- haveHelper := isSetuid(helperStat)
- systemName, present := uat.SystemAccountForBlessings(identityNames)
-
- switch {
- case haveHelper && present:
- return systemName, nil
- case haveHelper && !present:
- // The helper is owned by the device manager and installed as
- // setuid root. Therefore, the device manager must never run an
- // app as itself to prevent an app trivially granting itself
- // root permissions. There must be an associated uname for the
- // account in this case.
- return "", verror2.Make(verror2.NoAccess, ctx.Context(), "use of setuid helper requires an associated uname.")
- case !haveHelper:
- // When the helper is not setuid, the helper can't change the
- // app's uid so just run the app as the device manager's uname
- // whether or not there is an association.
- vlog.VI(1).Infof("helper not setuid. Device manager will invoke app with its own userid")
- user, err := user.Current()
- if err != nil {
- vlog.Errorf("user.Current() failed: %v", err)
- return "", verror2.Make(ErrOperationFailed, ctx.Context())
- }
- return user.Username, nil
- }
- return "", verror2.Make(ErrOperationFailed, ctx.Context())
-}
-
func genCmd(instanceDir, helperPath, systemName string, nsRoots []string) (*exec.Cmd, error) {
versionLink := filepath.Join(instanceDir, "version")
versionDir, err := filepath.EvalSymlinks(versionLink)
@@ -745,7 +694,15 @@
}
cmd := exec.Command(helperPath)
- cmd.Args = append(cmd.Args, "--username", systemName)
+
+ switch yes, err := suidHelper.suidhelperEnabled(systemName, helperPath); {
+ case err != nil:
+ return nil, err
+ case yes:
+ cmd.Args = append(cmd.Args, "--username", systemName)
+ case !yes:
+ cmd.Args = append(cmd.Args, "--username", systemName, "--dryrun")
+ }
var nsRootEnvs []string
for i, r := range nsRoots {
@@ -908,12 +865,7 @@
return nil, err
}
- systemName, err := systemAccountForHelper(call, helper, i.uat)
- if err != nil {
- cleanupDir(instanceDir, helper)
- return nil, err
- }
-
+ systemName := suidHelper.usernameForPrincipal(call, i.uat)
if err := saveSystemNameForInstance(instanceDir, systemName); err != nil {
cleanupDir(instanceDir, helper)
return nil, err
@@ -956,11 +908,7 @@
return err
}
- systemName, err := systemAccountForHelper(call, i.config.Helper, i.uat)
- if err != nil {
- return err
- }
-
+ systemName := suidHelper.usernameForPrincipal(call, i.uat)
startSystemName, err := readSystemNameForInstance(instanceDir)
if err != nil {
return err
diff --git a/services/mgmt/device/impl/helper_manager.go b/services/mgmt/device/impl/helper_manager.go
new file mode 100644
index 0000000..9e935d2
--- /dev/null
+++ b/services/mgmt/device/impl/helper_manager.go
@@ -0,0 +1,69 @@
+package impl
+
+import (
+ "os"
+ "os/user"
+
+ "v.io/core/veyron2/ipc"
+ "v.io/core/veyron2/verror2"
+ "v.io/core/veyron2/vlog"
+)
+
+type suidHelperState string
+
+var suidHelper suidHelperState
+
+func init() {
+ u, err := user.Current()
+ if err != nil {
+ vlog.Panicf("devicemanager has no current user: %v", err)
+ }
+ suidHelper = suidHelperState(u.Username)
+}
+
+// isSetuid is defined like this so we can override its
+// implementation for tests.
+var isSetuid = func(fileStat os.FileInfo) bool {
+ vlog.VI(2).Infof("running the original isSetuid")
+ return fileStat.Mode()&os.ModeSetuid == os.ModeSetuid
+}
+
+// unameRequiresSuidhelper determines if the the suidhelper must exist
+// and be setuid to run an application as system user un. If false, the
+// setuidhelper must be invoked with the --dryrun flag to skip making
+// system calls that will fail or provide apps with a trivial
+// priviledge escalation.
+func (dn suidHelperState) suidhelperEnabled(un, helperPath string) (bool, error) {
+ helperStat, err := os.Stat(helperPath)
+ if err != nil {
+ vlog.Errorf("Stat(%v) failed: %v. helper is required.", helperPath, err)
+ return false, verror2.Make(ErrOperationFailed, nil)
+ }
+ haveHelper := isSetuid(helperStat)
+
+ switch {
+ case haveHelper && string(dn) != un:
+ return true, nil
+ case haveHelper && string(dn) == un:
+ return false, verror2.Make(verror2.NoAccess, nil)
+ default:
+ return false, nil
+ }
+ // Keep the compiler happy.
+ return false, nil
+}
+
+// usernameForVanadiumPrincipal returns the system name that the
+// devicemanager will use to invoke apps.
+// TODO(rjkroege): This code assumes a desktop target and will need
+// to be reconsidered for embedded contexts.
+func (i suidHelperState) usernameForPrincipal(ctx ipc.ServerContext, uat BlessingSystemAssociationStore) string {
+ identityNames := ctx.RemoteBlessings().ForContext(ctx)
+ systemName, present := uat.SystemAccountForBlessings(identityNames)
+
+ if present {
+ return systemName
+ } else {
+ return string(i)
+ }
+}
diff --git a/services/mgmt/suidhelper/impl/args.go b/services/mgmt/suidhelper/impl/args.go
index 67003a3..185bae8 100644
--- a/services/mgmt/suidhelper/impl/args.go
+++ b/services/mgmt/suidhelper/impl/args.go
@@ -36,7 +36,7 @@
var (
flagUsername, flagWorkspace, flagLogDir, flagRun *string
flagMinimumUid *int64
- flagRemove *bool
+ flagRemove, flagDryrun *bool
)
func init() {
@@ -53,6 +53,7 @@
flagRun = sflag.Run
flagMinimumUid = sflag.MinimumUid
flagRemove = sflag.Remove
+ flagDryrun = sflag.Dryrun
}
// ParseArguments populates the WorkParameter object from the provided args
@@ -88,6 +89,8 @@
return fmt.Errorf("suidhelper does not permit uids less than %d", *flagMinimumUid)
}
+ wp.dryrun = *flagDryrun
+
// Preserve the arguments for examination by the test harness if executed
// in the course of a test.
if os.Getenv("VEYRON_SUIDHELPER_TEST") != "" {
diff --git a/services/mgmt/suidhelper/impl/args_test.go b/services/mgmt/suidhelper/impl/args_test.go
index 9358e9f..9341fc7 100644
--- a/services/mgmt/suidhelper/impl/args_test.go
+++ b/services/mgmt/suidhelper/impl/args_test.go
@@ -85,6 +85,24 @@
fmt.Errorf("suidhelper does not permit uids less than 501"),
WorkParameters{},
},
+
+ {
+ []string{"setuidhelper", "--minuid", "1", "--username", testUserName, "--workspace", "/hello",
+ "--logdir", "/logging", "--run", "/bin/veyron", "--dryrun", "--", "one", "two"},
+ []string{"A=B"},
+ nil,
+ WorkParameters{
+ uid: testUid,
+ gid: testGid,
+ workspace: "/hello",
+ logDir: "/logging",
+ argv0: "/bin/veyron",
+ argv: []string{"/bin/veyron", "one", "two"},
+ envv: []string{"A=B"},
+ dryrun: true,
+ remove: false,
+ },
+ },
}
for _, c := range cases {
diff --git a/services/mgmt/suidhelper/impl/flag/flag.go b/services/mgmt/suidhelper/impl/flag/flag.go
index f8b53e1..273f56b 100644
--- a/services/mgmt/suidhelper/impl/flag/flag.go
+++ b/services/mgmt/suidhelper/impl/flag/flag.go
@@ -15,7 +15,7 @@
var (
Username, Workspace, LogDir, Run *string
MinimumUid *int64
- Remove *bool
+ Remove, Dryrun *bool
)
func init() {
@@ -29,6 +29,7 @@
Run = fs.String("run", "", "Path to the application to exec.")
MinimumUid = fs.Int64("minuid", uidThreshold, "UIDs cannot be less than this number.")
Remove = fs.Bool("rm", false, "Remove the file trees given as command-line arguments.")
+ Dryrun = fs.Bool("dryrun", false, "Elides root-requiring systemcalls.")
}
const uidThreshold = 501
diff --git a/services/mgmt/suidhelper/impl/system.go b/services/mgmt/suidhelper/impl/system.go
index 60a7b92..8a93d91 100644
--- a/services/mgmt/suidhelper/impl/system.go
+++ b/services/mgmt/suidhelper/impl/system.go
@@ -34,22 +34,37 @@
}
func (hw *WorkParameters) Exec() error {
+ attr := new(syscall.ProcAttr)
+
+ if dir, err := os.Getwd(); err != nil {
+ log.Printf("error Getwd(): %v\n", err)
+ return fmt.Errorf("os.Getwd failed: %v", err)
+ attr.Dir = dir
+ }
+ attr.Env = hw.envv
+
if hw.dryrun {
log.Printf("[dryrun] syscall.Setgid(%d)", hw.gid)
log.Printf("[dryrun] syscall.Setuid(%d)", hw.uid)
} else {
- // NOTE(caprita): Commenting this out since it's broken with go
- // 1.4, to make the integration test pass. go/vcl/8240 will fix
- // it properly.
-
- // if err := syscall.Setgid(hw.gid); err != nil {
- // return fmt.Errorf("syscall.Setgid(%d) failed: %v", hw.gid, err)
- // }
- // if err := syscall.Setuid(hw.uid); err != nil {
- // return fmt.Errorf("syscall.Setuid(%d) failed: %v", hw.uid, err)
- // }
+ attr.Sys = new(syscall.SysProcAttr)
+ attr.Sys.Credential = new(syscall.Credential)
+ attr.Sys.Credential.Gid = uint32(hw.gid)
+ attr.Sys.Credential.Uid = uint32(hw.uid)
}
- return syscall.Exec(hw.argv0, hw.argv, hw.envv)
+
+ _, _, err := syscall.StartProcess(hw.argv0, hw.argv, attr)
+ if err != nil {
+ if !hw.dryrun {
+ log.Printf("StartProcess failed: attr: %#v, attr.Sys: %#v, attr.Sys.Cred: %#v error: %v\n", attr, attr.Sys, attr.Sys.Credential, err)
+ } else {
+ log.Printf("StartProcess failed: %v", err)
+ }
+ return fmt.Errorf("syscall.StartProcess(%s) failed: %v", hw.argv0, err)
+ }
+ // TODO(rjkroege): Return the pid to the node manager.
+ os.Exit(0)
+ return nil // Not reached.
}
func (hw *WorkParameters) Remove() error {