Merge "services/device/internal: Allow re-run'ing of apps to work"
diff --git a/services/device/device/doc.go b/services/device/device/doc.go
index cd1ba14..53c9a17 100644
--- a/services/device/device/doc.go
+++ b/services/device/device/doc.go
@@ -41,6 +41,9 @@
 
  -alsologtostderr=true
    log to standard error as well as files
+ -chown=false
+   Change owner of files and directories given as command-line arguments to the
+   user specified by this flag
  -dryrun=false
    Elides root-requiring systemcalls.
  -kill=false
diff --git a/services/device/deviced/doc.go b/services/device/deviced/doc.go
index 567ed5c..3468792 100644
--- a/services/device/deviced/doc.go
+++ b/services/device/deviced/doc.go
@@ -43,6 +43,9 @@
 
  -alsologtostderr=true
    log to standard error as well as files
+ -chown=false
+   Change owner of files and directories given as command-line arguments to the
+   user specified by this flag
  -dryrun=false
    Elides root-requiring systemcalls.
  -kill=false
diff --git a/services/device/internal/impl/app_service.go b/services/device/internal/impl/app_service.go
index 3f2b49d..066b667 100644
--- a/services/device/internal/impl/app_service.go
+++ b/services/device/internal/impl/app_service.go
@@ -766,6 +766,7 @@
 	saArgs.workspace = rootDir
 
 	logDir := filepath.Join(instanceDir, "logs")
+	suidHelper.chownTree(suidHelper.getCurrentUser(), instanceDir, os.Stdout, os.Stdin)
 	if err := mkdirPerm(logDir, 0755); err != nil {
 		return nil, err
 	}
diff --git a/services/device/internal/impl/helper_manager.go b/services/device/internal/impl/helper_manager.go
index f4369eb..280db2b 100644
--- a/services/device/internal/impl/helper_manager.go
+++ b/services/device/internal/impl/helper_manager.go
@@ -40,6 +40,10 @@
 	}
 }
 
+func (s suidHelperState) getCurrentUser() string {
+	return s.dmUser
+}
+
 // terminatePid sends a SIGKILL to the target pid
 func (s suidHelperState) terminatePid(pid int, stdout, stderr io.Writer) error {
 	if err := s.internalModalOp(stdout, stderr, "--kill", strconv.Itoa(pid)); err != nil {
@@ -56,6 +60,16 @@
 	return nil
 }
 
+// chown files or directories
+func (s suidHelperState) chownTree(username string, dirOrFile string, stdout, stderr io.Writer) error {
+	args := []string{"--chown", "--username", username, dirOrFile}
+
+	if err := s.internalModalOp(stdout, stderr, args...); err != nil {
+		return fmt.Errorf("devicemanager's invocation of suidhelper chown %v failed: %v", dirOrFile, err)
+	}
+	return nil
+}
+
 type suidAppCmdArgs struct {
 	// args to helper
 	targetUser, progname, workspace, logdir, binpath string
@@ -116,6 +130,7 @@
 
 	if err := cmd.Run(); err != nil {
 		vlog.Errorf("failed calling helper with args (%v):%v", arg, err)
+		return err
 	}
 	return nil
 }
diff --git a/services/device/internal/suid/args.go b/services/device/internal/suid/args.go
index 10ccfac..1473654 100644
--- a/services/device/internal/suid/args.go
+++ b/services/device/internal/suid/args.go
@@ -8,6 +8,7 @@
 	"bytes"
 	"encoding/json"
 	"flag"
+	"fmt"
 	"os"
 	"os/user"
 	"strconv"
@@ -38,6 +39,7 @@
 	envv      []string
 	dryrun    bool
 	remove    bool
+	chown     bool
 	kill      bool
 	killPids  []int
 }
@@ -54,7 +56,7 @@
 var (
 	flagUsername, flagWorkspace, flagLogDir, flagRun, flagProgName *string
 	flagMinimumUid                                                 *int64
-	flagRemove, flagKill, flagDryrun                               *bool
+	flagRemove, flagKill, flagChown, flagDryrun                    *bool
 )
 
 func init() {
@@ -71,6 +73,7 @@
 	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.")
 }
 
@@ -84,29 +87,62 @@
 	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 {
-	// --rm and --kill are modal. Complain if any other flag is set along with one of those.
-	if *flagRemove || *flagKill {
-		// 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"
-		}
-
-		counter := 0
-		fs.Visit(func(f *flag.Flag) {
-			if flagsToIgnore[f.Name] != f.Value.String() {
-				counter++
-			}
-		})
-
-		if counter > 1 {
-			return verror.New(errInvalidFlags, nil, counter, "--rm and --kill cannot be used with any other flag")
-		}
+	if err := checkFlagCombinations(fs); err != nil {
+		return err
 	}
 
 	if *flagRemove {
@@ -146,15 +182,25 @@
 	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") != "" {
@@ -171,8 +217,6 @@
 		wp.dryrun = true
 	}
 
-	wp.uid = int(uid)
-	wp.gid = int(gid)
 	wp.workspace = *flagWorkspace
 	wp.argv0 = *flagRun
 	wp.logDir = *flagLogDir
diff --git a/services/device/internal/suid/args_test.go b/services/device/internal/suid/args_test.go
index 092c1ab..491a171 100644
--- a/services/device/internal/suid/args_test.go
+++ b/services/device/internal/suid/args_test.go
@@ -41,6 +41,7 @@
 				envv:      []string{"A=B"},
 				dryrun:    false,
 				remove:    false,
+				chown:     false,
 				kill:      false,
 				killPids:  nil,
 			},
@@ -61,6 +62,7 @@
 				envv:      []string{"A=B"},
 				dryrun:    false,
 				remove:    false,
+				chown:     false,
 				kill:      false,
 				killPids:  nil,
 			},
@@ -87,6 +89,27 @@
 				envv:      nil,
 				dryrun:    false,
 				remove:    true,
+				chown:     false,
+				kill:      false,
+				killPids:  nil,
+			},
+		},
+
+		{
+			[]string{"setuidhelper", "--chown", "--username", testUserName, "--dryrun", "--minuid", "1", "/tmp/foo", "/tmp/bar"},
+			[]string{"A=B"},
+			"",
+			WorkParameters{
+				uid:       testUid,
+				gid:       testGid,
+				workspace: "",
+				logDir:    "",
+				argv0:     "",
+				argv:      []string{"/tmp/foo", "/tmp/bar"},
+				envv:      nil,
+				dryrun:    true,
+				remove:    false,
+				chown:     true,
 				kill:      false,
 				killPids:  nil,
 			},
@@ -106,6 +129,7 @@
 				envv:      nil,
 				dryrun:    false,
 				remove:    false,
+				chown:     false,
 				kill:      true,
 				killPids:  []int{235, 451},
 			},
@@ -125,6 +149,7 @@
 				envv:      nil,
 				dryrun:    false,
 				remove:    false,
+				chown:     false,
 				kill:      true,
 				killPids:  nil,
 			},
@@ -145,6 +170,7 @@
 				envv:      []string{"A=B"},
 				dryrun:    true,
 				remove:    false,
+				chown:     false,
 				kill:      false,
 				killPids:  nil,
 			},
diff --git a/services/device/internal/suid/system.go b/services/device/internal/suid/system.go
index 6aad84b..285c4e3 100644
--- a/services/device/internal/suid/system.go
+++ b/services/device/internal/suid/system.go
@@ -39,8 +39,15 @@
 		return os.Chown(path, hw.uid, hw.gid)
 	}
 
-	for _, p := range []string{hw.workspace, hw.logDir} {
+	chownPaths := hw.argv
+	if !hw.chown {
+		// Chown was invoked as part of regular suid execution, rather than directly
+		// via --chown. In that case, we chown the workspace and log directory
 		// TODO(rjkroege): Ensure that the device manager can read log entries.
+		chownPaths = []string{hw.workspace, hw.logDir}
+	}
+
+	for _, p := range chownPaths {
 		if err := filepath.Walk(p, chown); err != nil {
 			return verror.New(errChownFailed, nil, p, hw.uid, hw.gid, err)
 		}