diff options
Diffstat (limited to 'src/syscall/exec_unix_test.go')
-rw-r--r-- | src/syscall/exec_unix_test.go | 401 |
1 files changed, 401 insertions, 0 deletions
diff --git a/src/syscall/exec_unix_test.go b/src/syscall/exec_unix_test.go new file mode 100644 index 0000000..9627317 --- /dev/null +++ b/src/syscall/exec_unix_test.go @@ -0,0 +1,401 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix + +package syscall_test + +import ( + "bytes" + "fmt" + "internal/testenv" + "io" + "math/rand" + "os" + "os/exec" + "os/signal" + "strconv" + "syscall" + "testing" + "time" + "unsafe" +) + +type command struct { + pipe io.WriteCloser + proc *exec.Cmd + test *testing.T +} + +func (c *command) Info() (pid, pgrp int) { + pid = c.proc.Process.Pid + + pgrp, err := syscall.Getpgid(pid) + if err != nil { + c.test.Fatal(err) + } + + return +} + +func (c *command) Start() { + if err := c.proc.Start(); err != nil { + c.test.Fatal(err) + } +} + +func (c *command) Stop() { + c.pipe.Close() + if err := c.proc.Wait(); err != nil { + c.test.Fatal(err) + } +} + +func create(t *testing.T) *command { + testenv.MustHaveExec(t) + + proc := exec.Command("cat") + stdin, err := proc.StdinPipe() + if err != nil { + t.Fatal(err) + } + + return &command{stdin, proc, t} +} + +func parent() (pid, pgrp int) { + return syscall.Getpid(), syscall.Getpgrp() +} + +func TestZeroSysProcAttr(t *testing.T) { + ppid, ppgrp := parent() + + cmd := create(t) + + cmd.Start() + defer cmd.Stop() + + cpid, cpgrp := cmd.Info() + + if cpid == ppid { + t.Fatalf("Parent and child have the same process ID") + } + + if cpgrp != ppgrp { + t.Fatalf("Child is not in parent's process group") + } +} + +func TestSetpgid(t *testing.T) { + ppid, ppgrp := parent() + + cmd := create(t) + + cmd.proc.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + cmd.Start() + defer cmd.Stop() + + cpid, cpgrp := cmd.Info() + + if cpid == ppid { + t.Fatalf("Parent and child have the same process ID") + } + + if cpgrp == ppgrp { + t.Fatalf("Parent and child are in the same process group") + } + + if cpid != cpgrp { + t.Fatalf("Child's process group is not the child's process ID") + } +} + +func TestPgid(t *testing.T) { + ppid, ppgrp := parent() + + cmd1 := create(t) + + cmd1.proc.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + cmd1.Start() + defer cmd1.Stop() + + cpid1, cpgrp1 := cmd1.Info() + + if cpid1 == ppid { + t.Fatalf("Parent and child 1 have the same process ID") + } + + if cpgrp1 == ppgrp { + t.Fatalf("Parent and child 1 are in the same process group") + } + + if cpid1 != cpgrp1 { + t.Fatalf("Child 1's process group is not its process ID") + } + + cmd2 := create(t) + + cmd2.proc.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + Pgid: cpgrp1, + } + cmd2.Start() + defer cmd2.Stop() + + cpid2, cpgrp2 := cmd2.Info() + + if cpid2 == ppid { + t.Fatalf("Parent and child 2 have the same process ID") + } + + if cpgrp2 == ppgrp { + t.Fatalf("Parent and child 2 are in the same process group") + } + + if cpid2 == cpgrp2 { + t.Fatalf("Child 2's process group is its process ID") + } + + if cpid1 == cpid2 { + t.Fatalf("Child 1 and 2 have the same process ID") + } + + if cpgrp1 != cpgrp2 { + t.Fatalf("Child 1 and 2 are not in the same process group") + } +} + +func TestForeground(t *testing.T) { + signal.Ignore(syscall.SIGTTIN, syscall.SIGTTOU) + defer signal.Reset() + + tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) + if err != nil { + t.Skipf("Can't test Foreground. Couldn't open /dev/tty: %s", err) + } + defer tty.Close() + + // This should really be pid_t, however _C_int (aka int32) is generally + // equivalent. + fpgrp := int32(0) + + errno := syscall.IoctlPtr(tty.Fd(), syscall.TIOCGPGRP, unsafe.Pointer(&fpgrp)) + if errno != 0 { + t.Fatalf("TIOCGPGRP failed with error code: %s", errno) + } + + if fpgrp == 0 { + t.Fatalf("Foreground process group is zero") + } + + ppid, ppgrp := parent() + + cmd := create(t) + + cmd.proc.SysProcAttr = &syscall.SysProcAttr{ + Ctty: int(tty.Fd()), + Foreground: true, + } + cmd.Start() + + cpid, cpgrp := cmd.Info() + + if cpid == ppid { + t.Fatalf("Parent and child have the same process ID") + } + + if cpgrp == ppgrp { + t.Fatalf("Parent and child are in the same process group") + } + + if cpid != cpgrp { + t.Fatalf("Child's process group is not the child's process ID") + } + + cmd.Stop() + + // This call fails on darwin/arm64. The failure doesn't matter, though. + // This is just best effort. + syscall.IoctlPtr(tty.Fd(), syscall.TIOCSPGRP, unsafe.Pointer(&fpgrp)) +} + +func TestForegroundSignal(t *testing.T) { + tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) + if err != nil { + t.Skipf("couldn't open /dev/tty: %s", err) + } + defer tty.Close() + + // This should really be pid_t, however _C_int (aka int32) is generally + // equivalent. + fpgrp := int32(0) + + errno := syscall.IoctlPtr(tty.Fd(), syscall.TIOCGPGRP, unsafe.Pointer(&fpgrp)) + if errno != 0 { + t.Fatalf("TIOCGPGRP failed with error code: %s", errno) + } + + if fpgrp == 0 { + t.Fatalf("Foreground process group is zero") + } + + defer func() { + signal.Ignore(syscall.SIGTTIN, syscall.SIGTTOU) + syscall.IoctlPtr(tty.Fd(), syscall.TIOCSPGRP, unsafe.Pointer(&fpgrp)) + signal.Reset() + }() + + ch1 := make(chan os.Signal, 1) + ch2 := make(chan bool) + + signal.Notify(ch1, syscall.SIGTTIN, syscall.SIGTTOU) + defer signal.Stop(ch1) + + cmd := create(t) + + go func() { + cmd.proc.SysProcAttr = &syscall.SysProcAttr{ + Ctty: int(tty.Fd()), + Foreground: true, + } + cmd.Start() + cmd.Stop() + close(ch2) + }() + + timer := time.NewTimer(30 * time.Second) + defer timer.Stop() + for { + select { + case sig := <-ch1: + t.Errorf("unexpected signal %v", sig) + case <-ch2: + // Success. + return + case <-timer.C: + t.Fatal("timed out waiting for child process") + } + } +} + +// Test a couple of cases that SysProcAttr can't handle. Issue 29458. +func TestInvalidExec(t *testing.T) { + t.Parallel() + t.Run("SetCtty-Foreground", func(t *testing.T) { + t.Parallel() + cmd := create(t) + cmd.proc.SysProcAttr = &syscall.SysProcAttr{ + Setctty: true, + Foreground: true, + Ctty: 0, + } + if err := cmd.proc.Start(); err == nil { + t.Error("expected error setting both SetCtty and Foreground") + } + }) + t.Run("invalid-Ctty", func(t *testing.T) { + t.Parallel() + cmd := create(t) + cmd.proc.SysProcAttr = &syscall.SysProcAttr{ + Setctty: true, + Ctty: 3, + } + if err := cmd.proc.Start(); err == nil { + t.Error("expected error with invalid Ctty value") + } + }) +} + +// TestExec is for issue #41702. +func TestExec(t *testing.T) { + testenv.MustHaveExec(t) + cmd := exec.Command(os.Args[0], "-test.run=TestExecHelper") + cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=2") + o, err := cmd.CombinedOutput() + if err != nil { + t.Errorf("%s\n%v", o, err) + } +} + +// TestExecHelper is used by TestExec. It does nothing by itself. +// In testing on macOS 10.14, this used to fail with +// "signal: illegal instruction" more than half the time. +func TestExecHelper(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "2" { + return + } + + // We don't have to worry about restoring these values. + // We are in a child process that only runs this test, + // and we are going to call syscall.Exec anyhow. + os.Setenv("GO_WANT_HELPER_PROCESS", "3") + + stop := time.Now().Add(time.Second) + for i := 0; i < 100; i++ { + go func(i int) { + r := rand.New(rand.NewSource(int64(i))) + for time.Now().Before(stop) { + r.Uint64() + } + }(i) + } + + time.Sleep(10 * time.Millisecond) + + argv := []string{os.Args[0], "-test.run=TestExecHelper"} + syscall.Exec(os.Args[0], argv, os.Environ()) + + t.Error("syscall.Exec returned") +} + +// Test that rlimit values are restored by exec. +func TestRlimitRestored(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "" { + fmt.Println(syscall.OrigRlimitNofile().Cur) + os.Exit(0) + } + + orig := syscall.OrigRlimitNofile() + if orig.Cur == 0 { + t.Skip("skipping test because rlimit not adjusted at startup") + } + + executable, err := os.Executable() + if err != nil { + executable = os.Args[0] + } + + cmd := testenv.Command(t, executable, "-test.run=TestRlimitRestored") + cmd = testenv.CleanCmdEnv(cmd) + cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1") + + out, err := cmd.CombinedOutput() + if len(out) > 0 { + t.Logf("%s", out) + } + if err != nil { + t.Fatalf("subprocess failed: %v", err) + } + s := string(bytes.TrimSpace(out)) + v, err := strconv.ParseUint(s, 10, 64) + if err != nil { + t.Fatalf("could not parse %q as number: %v", s, v) + } + + if v != uint64(orig.Cur) { + t.Errorf("exec rlimit = %d, want %d", v, orig) + } +} + +func TestForkExecNilArgv(t *testing.T) { + defer func() { + if p := recover(); p != nil { + t.Fatal("forkExec panicked") + } + }() + + // We don't really care what the result of forkExec is, just that it doesn't + // panic, so we choose something we know won't actually spawn a process (probably). + syscall.ForkExec("/dev/null", nil, nil) +} |