package main import ( "bytes" "context" "fmt" "io" "os/exec" "sort" "strings" "time" ) // Root repeats an act with more rights than the account this runs as has (novox/hq ADR 0394 ยง2). It is an // interface so a unit test uses a fake: no test runs sudo. type Root interface { // Run runs one fixed binary with the arguments given, never through a shell, and answers what it wrote. Run(bin string, args ...string) ([]byte, error) } // rootBins are the only binaries that run as root. The argument list is the caller's, but each caller in this // module builds it from fixed words and one path, and ends its options with `--`. var rootBins = map[string]bool{ "/usr/bin/cat": true, "/usr/bin/cp": true, "/usr/bin/mv": true, "/usr/bin/stat": true, "/usr/bin/chmod": true, "/usr/bin/chown": true, "/usr/bin/mkdir": true, "/usr/bin/rm": true, "/usr/bin/readlink": true, "/usr/bin/find": true, } func rootBinList() string { var l []string for b := range rootBins { l = append(l, b) } sort.Strings(l) return strings.Join(l, ", ") } // sudoRoot is the real runner: `sudo -n` (never a password prompt) with a binary from rootBins. type sudoRoot struct{} // capped keeps at most n bytes and drops the rest. type capped struct { buf bytes.Buffer n int } func (c *capped) Write(p []byte) (int, error) { if room := c.n - c.buf.Len(); room > 0 { if len(p) < room { room = len(p) } c.buf.Write(p[:room]) } return len(p), nil } func (sudoRoot) Run(bin string, args ...string) ([]byte, error) { if !rootBins[bin] { return nil, fmt.Errorf("%s is not one of the binaries that run as root here (%s)", bin, rootBinList()) } ctx, cancel := context.WithTimeout(context.Background(), SearchTime+10*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "/usr/bin/sudo", append([]string{"-n", "--", bin}, args...)...) out, errs := &capped{n: MaxScan}, &capped{n: 4096} cmd.Stdout, cmd.Stderr = out, errs cmd.Stdin = io.LimitReader(strings.NewReader(""), 0) if err := cmd.Run(); err != nil { msg := strings.TrimSpace(errs.buf.String()) if msg == "" { msg = err.Error() } return nil, fmt.Errorf("sudo -n %s: %s", bin, msg) } return out.buf.Bytes(), nil }