lib/suid: Add kill functionality.

Add --kill to suidhelper, to be used by the device manager to stop apps
owned by other users

Change-Id: I2e0e3dd2437db855f903dd2fe3b9dc233f2ab03b
diff --git a/services/device/device/doc.go b/services/device/device/doc.go
index dbf6677..81a7276 100644
--- a/services/device/device/doc.go
+++ b/services/device/device/doc.go
@@ -38,6 +38,8 @@
    log to standard error as well as files
  -dryrun=false
    Elides root-requiring systemcalls.
+ -kill=false
+   Kill process ids given as command-line arguments.
  -log_backtrace_at=:0
    when logging hits line file:N, emit a stack trace
  -log_dir=
diff --git a/services/device/internal/impl/device_installer.go b/services/device/internal/impl/device_installer.go
index 9c0a322..4d66a49 100644
--- a/services/device/internal/impl/device_installer.go
+++ b/services/device/internal/impl/device_installer.go
@@ -284,6 +284,12 @@
 	}
 	cmd := exec.Command(helperPath)
 	cmd.Args = append(cmd.Args, "--rm", root)
+	if stderr != nil {
+		cmd.Stderr = stderr
+	}
+	if stdout != nil {
+		cmd.Stdout = stdout
+	}
 	if err := cmd.Run(); err != nil {
 		return fmt.Errorf("devicemanager's invocation of suidhelper to remove(%v) failed: %v", root, err)
 	}
diff --git a/services/device/internal/suid/args.go b/services/device/internal/suid/args.go
index 6a9ab66..10ccfac 100644
--- a/services/device/internal/suid/args.go
+++ b/services/device/internal/suid/args.go
@@ -24,6 +24,8 @@
 	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 {
@@ -36,6 +38,8 @@
 	envv      []string
 	dryrun    bool
 	remove    bool
+	kill      bool
+	killPids  []int
 }
 
 type ArgsSavedForTest struct {
@@ -50,7 +54,7 @@
 var (
 	flagUsername, flagWorkspace, flagLogDir, flagRun, flagProgName *string
 	flagMinimumUid                                                 *int64
-	flagRemove, flagDryrun                                         *bool
+	flagRemove, flagKill, flagDryrun                               *bool
 )
 
 func init() {
@@ -66,6 +70,7 @@
 	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.")
 	flagDryrun = fs.Bool("dryrun", false, "Elides root-requiring systemcalls.")
 }
 
@@ -82,12 +87,47 @@
 // 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 *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
+	}
+
 	username := *flagUsername
 	if username == "" {
 		return verror.New(errUserNameMissing, nil)
diff --git a/services/device/internal/suid/args_test.go b/services/device/internal/suid/args_test.go
index 5476a06..092c1ab 100644
--- a/services/device/internal/suid/args_test.go
+++ b/services/device/internal/suid/args_test.go
@@ -41,6 +41,8 @@
 				envv:      []string{"A=B"},
 				dryrun:    false,
 				remove:    false,
+				kill:      false,
+				killPids:  nil,
 			},
 		},
 
@@ -59,8 +61,11 @@
 				envv:      []string{"A=B"},
 				dryrun:    false,
 				remove:    false,
+				kill:      false,
+				killPids:  nil,
 			},
 		},
+
 		{
 			[]string{"setuidhelper", "--username", testUserName},
 			[]string{"A=B"},
@@ -82,13 +87,47 @@
 				envv:      nil,
 				dryrun:    false,
 				remove:    true,
+				kill:      false,
+				killPids:  nil,
 			},
 		},
+
 		{
-			[]string{"setuidhelper", "--username", testUserName},
+			[]string{"setuidhelper", "--kill", "235", "451"},
 			[]string{"A=B"},
-			errUIDTooLow.ID,
-			WorkParameters{},
+			"",
+			WorkParameters{
+				uid:       0,
+				gid:       0,
+				workspace: "",
+				logDir:    "",
+				argv0:     "",
+				argv:      nil,
+				envv:      nil,
+				dryrun:    false,
+				remove:    false,
+				kill:      true,
+				killPids:  []int{235, 451},
+			},
+		},
+
+		{
+			[]string{"setuidhelper", "--kill", "235", "451oops"},
+			[]string{"A=B"},
+			errAtoiFailed.ID,
+			WorkParameters{
+				uid:       0,
+				gid:       0,
+				workspace: "",
+				logDir:    "",
+				argv0:     "",
+				argv:      nil,
+				envv:      nil,
+				dryrun:    false,
+				remove:    false,
+				kill:      true,
+				killPids:  nil,
+			},
 		},
 
 		{
@@ -106,6 +145,8 @@
 				envv:      []string{"A=B"},
 				dryrun:    true,
 				remove:    false,
+				kill:      false,
+				killPids:  nil,
 			},
 		},
 	}
diff --git a/services/device/internal/suid/run.go b/services/device/internal/suid/run.go
index bedc968..3814353 100644
--- a/services/device/internal/suid/run.go
+++ b/services/device/internal/suid/run.go
@@ -18,6 +18,10 @@
 		return work.Remove()
 	}
 
+	if work.kill {
+		return work.Kill()
+	}
+
 	if err := work.Chown(); err != nil {
 		return err
 	}
diff --git a/services/device/internal/suid/system.go b/services/device/internal/suid/system.go
index de37d54..6aad84b 100644
--- a/services/device/internal/suid/system.go
+++ b/services/device/internal/suid/system.go
@@ -21,6 +21,8 @@
 	errGetwdFailed        = verror.Register(pkgPath+".errGetwdFailed", verror.NoRetry, "{1:}{2:} os.Getwd failed{:_}")
 	errStartProcessFailed = verror.Register(pkgPath+".errStartProcessFailed", verror.NoRetry, "{1:}{2:} syscall.StartProcess({3}) failed{:_}")
 	errRemoveAllFailed    = verror.Register(pkgPath+".errRemoveAllFailed", verror.NoRetry, "{1:}{2:} os.RemoveAll({3}) failed{:_}")
+	errFindProcessFailed  = verror.Register(pkgPath+".errFindProcessFailed", verror.NoRetry, "{1:}{2:} os.FindProcess({3}) failed{:_}")
+	errKillFailed         = verror.Register(pkgPath+".errKillFailed", verror.NoRetry, "{1:}{2:} os.Process.Kill({3}) failed{:_}")
 )
 
 // Chown is only availabe on UNIX platforms so this file has a build
@@ -106,3 +108,17 @@
 	}
 	return nil
 }
+
+func (hw *WorkParameters) Kill() error {
+	for _, pid := range hw.killPids {
+		proc, err := os.FindProcess(pid)
+		if err != nil {
+			return verror.New(errFindProcessFailed, nil, pid, err)
+		}
+
+		if err = proc.Kill(); err != nil {
+			return verror.New(errKillFailed, nil, pid, err)
+		}
+	}
+	return nil
+}