"os"
"os/exec"
"os/signal"
+ "os/user"
"path/filepath"
+ "strconv"
+ "strings"
"syscall"
"testing"
- "time"
)
-func TestDeathSignal(t *testing.T) {
- if os.Getuid() != 0 {
- t.Skip("skipping root only test")
- }
- if testing.Short() && testenv.Builder() != "" && os.Getenv("USER") == "swarming" {
- // The Go build system's swarming user is known not to be root.
- // Unfortunately, it sometimes appears as root due the current
- // implementation of a no-network check using 'unshare -n -r'.
- // Since this test does need root to work, we need to skip it.
- t.Skip("skipping root only test on a non-root builder")
+// TestDeathSignalSetuid verifies that a command run with a different UID still
+// receives PDeathsig; it is a regression test for https://go.dev/issue/9686.
+func TestDeathSignalSetuid(t *testing.T) {
+ if testing.Short() {
+ t.Skipf("skipping test that copies its binary into temp dir")
}
- // Copy the test binary to a location that a non-root user can read/execute
- // after we drop privileges
+ // Copy the test binary to a location that another user can read/execute
+ // after we drop privileges.
+ //
+ // TODO(bcmills): Why do we believe that another users will be able to
+ // execute a binary in this directory? (It could be mounted noexec.)
tempDir, err := os.MkdirTemp("", "TestDeathSignal")
if err != nil {
t.Fatalf("cannot create temporary directory: %v", err)
t.Fatalf("failed to close test binary %q, %v", tmpBinary, err)
}
- cmd := exec.Command(tmpBinary)
- cmd.Env = append(os.Environ(), "GO_DEATHSIG_PARENT=1")
+ cmd := testenv.Command(t, tmpBinary)
+ cmd.Env = append(cmd.Environ(), "GO_DEATHSIG_PARENT=1")
chldStdin, err := cmd.StdinPipe()
if err != nil {
t.Fatalf("failed to create new stdin pipe: %v", err)
if err != nil {
t.Fatalf("failed to create new stdout pipe: %v", err)
}
- cmd.Stderr = os.Stderr
+ stderr := new(strings.Builder)
+ cmd.Stderr = stderr
err = cmd.Start()
- defer cmd.Wait()
+ defer func() {
+ chldStdin.Close()
+ cmd.Wait()
+ if stderr.Len() > 0 {
+ t.Logf("stderr:\n%s", stderr)
+ }
+ }()
if err != nil {
t.Fatalf("failed to start first child process: %v", err)
}
if got, err := chldPipe.ReadString('\n'); got == "start\n" {
syscall.Kill(cmd.Process.Pid, syscall.SIGTERM)
- go func() {
- time.Sleep(5 * time.Second)
- chldStdin.Close()
- }()
-
want := "ok\n"
if got, err = chldPipe.ReadString('\n'); got != want {
t.Fatalf("expected %q, received %q, %v", want, got, err)
}
+ } else if got == "skip\n" {
+ t.Skipf("skipping: parent could not run child program as selected user")
} else {
t.Fatalf("did not receive start from child, received %q, %v", got, err)
}
}
func deathSignalParent() {
+ var (
+ u *user.User
+ err error
+ )
+ if os.Getuid() == 0 {
+ tryUsers := []string{"nobody"}
+ if testenv.Builder() != "" {
+ tryUsers = append(tryUsers, "gopher")
+ }
+ for _, name := range tryUsers {
+ u, err = user.Lookup(name)
+ if err == nil {
+ break
+ }
+ fmt.Fprintf(os.Stderr, "Lookup(%q): %v\n", name, err)
+ }
+ }
+ if u == nil {
+ // If we couldn't find an unprivileged user to run as, try running as
+ // the current user. (Empirically this still causes the call to Start to
+ // fail with a permission error if running as a non-root user on Linux.)
+ u, err = user.Current()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ }
+
+ uid, err := strconv.ParseUint(u.Uid, 10, 32)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "invalid UID: %v\n", err)
+ os.Exit(1)
+ }
+ gid, err := strconv.ParseUint(u.Gid, 10, 32)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "invalid GID: %v\n", err)
+ os.Exit(1)
+ }
+
cmd := exec.Command(os.Args[0])
cmd.Env = append(os.Environ(),
"GO_DEATHSIG_PARENT=",
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
attrs := syscall.SysProcAttr{
- Pdeathsig: syscall.SIGUSR1,
- // UID/GID 99 is the user/group "nobody" on RHEL/Fedora and is
- // unused on Ubuntu
- Credential: &syscall.Credential{Uid: 99, Gid: 99},
+ Pdeathsig: syscall.SIGUSR1,
+ Credential: &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)},
}
cmd.SysProcAttr = &attrs
- err := cmd.Start()
- if err != nil {
- fmt.Fprintf(os.Stderr, "death signal parent error: %v\n", err)
+ fmt.Fprintf(os.Stderr, "starting process as user %q\n", u.Username)
+ if err := cmd.Start(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ if testenv.SyscallIsNotSupported(err) {
+ fmt.Println("skip")
+ os.Exit(0)
+ }
os.Exit(1)
}
cmd.Wait()