diff --git a/src/syscall/exec_freebsd.go b/src/syscall/exec_freebsd.go index 393b3d485bbdb0..b2650fc56379d6 100644 --- a/src/syscall/exec_freebsd.go +++ b/src/syscall/exec_freebsd.go @@ -29,9 +29,16 @@ type SysProcAttr struct { // Unlike Setctty, in this case Ctty must be a descriptor // number in the parent process. Foreground bool - Pgid int // Child's process group ID if Setpgid. + Pgid int // Child's process group ID if Setpgid. + Pdeathsig Signal // Signal that the process will get when its parent dies (Linux and FreeBSD only) } +const ( + _P_PID = 0 + + _PROC_PDEATHSIG_CTL = 11 +) + // Implemented in runtime package. func runtime_BeforeFork() func runtime_AfterFork() @@ -57,6 +64,9 @@ func forkAndExecInChild(argv0 *byte, argv, envv []*byte, chroot, dir *byte, attr i int ) + // Record parent PID so child can test if it has died. + ppid, _, _ := RawSyscall(SYS_GETPID, 0, 0, 0) + // guard against side effects of shuffling fds below. // Make sure that nextfd is beyond any currently open files so // that we can't run the risk of overwriting any of them. @@ -175,6 +185,26 @@ func forkAndExecInChild(argv0 *byte, argv, envv []*byte, chroot, dir *byte, attr } } + // Parent death signal + if sys.Pdeathsig != 0 { + _, _, err1 = RawSyscall6(SYS_PROCCTL, _P_PID, _PROC_PDEATHSIG_CTL, uintptr(unsafe.Pointer(&sys.Pdeathsig)), 0, 0, 0) + if err1 != 0 { + goto childerror + } + + // Signal self if parent is already dead. This might cause a + // duplicate signal in rare cases, but it won't matter when + // using SIGKILL. + r1, _, _ = RawSyscall(SYS_GETPPID, 0, 0, 0) + if r1 != ppid { + pid, _, _ := RawSyscall(SYS_GETPID, 0, 0, 0) + _, _, err1 := RawSyscall(SYS_KILL, pid, uintptr(sys.Pdeathsig), 0) + if err1 != 0 { + goto childerror + } + } + } + // Pass 1: look for fd[i] < i and move those up above len(fd) // so that pass 2 won't stomp on an fd it needs later. if pipe < nextfd { diff --git a/src/syscall/exec_linux.go b/src/syscall/exec_linux.go index dfc522854560bd..e1506e1e2b792e 100644 --- a/src/syscall/exec_linux.go +++ b/src/syscall/exec_linux.go @@ -46,7 +46,7 @@ type SysProcAttr struct { // number in the parent process. Foreground bool Pgid int // Child's process group ID if Setpgid. - Pdeathsig Signal // Signal that the process will get when its parent dies (Linux only) + Pdeathsig Signal // Signal that the process will get when its parent dies (Linux and FreeBSD only) Cloneflags uintptr // Flags for clone calls (Linux only) Unshareflags uintptr // Flags for unshare calls (Linux only) UidMappings []SysProcIDMap // User ID mappings for user namespaces. diff --git a/src/syscall/exec_pdeathsig_test.go b/src/syscall/exec_pdeathsig_test.go new file mode 100644 index 00000000000000..6533d3a1389f90 --- /dev/null +++ b/src/syscall/exec_pdeathsig_test.go @@ -0,0 +1,135 @@ +// 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 freebsd || linux +// +build freebsd linux + +package syscall_test + +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" + "os/signal" + "path/filepath" + "syscall" + "testing" + "time" +) + +func TestDeathSignal(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("skipping root only test") + } + + // Copy the test binary to a location that a non-root user can read/execute + // after we drop privileges + tempDir, err := os.MkdirTemp("", "TestDeathSignal") + if err != nil { + t.Fatalf("cannot create temporary directory: %v", err) + } + defer os.RemoveAll(tempDir) + os.Chmod(tempDir, 0755) + + tmpBinary := filepath.Join(tempDir, filepath.Base(os.Args[0])) + + src, err := os.Open(os.Args[0]) + if err != nil { + t.Fatalf("cannot open binary %q, %v", os.Args[0], err) + } + defer src.Close() + + dst, err := os.OpenFile(tmpBinary, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + t.Fatalf("cannot create temporary binary %q, %v", tmpBinary, err) + } + if _, err := io.Copy(dst, src); err != nil { + t.Fatalf("failed to copy test binary to %q, %v", tmpBinary, err) + } + err = dst.Close() + if err != nil { + t.Fatalf("failed to close test binary %q, %v", tmpBinary, err) + } + + cmd := exec.Command(tmpBinary) + cmd.Env = append(os.Environ(), "GO_DEATHSIG_PARENT=1") + chldStdin, err := cmd.StdinPipe() + if err != nil { + t.Fatalf("failed to create new stdin pipe: %v", err) + } + chldStdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatalf("failed to create new stdout pipe: %v", err) + } + cmd.Stderr = os.Stderr + + err = cmd.Start() + defer cmd.Wait() + if err != nil { + t.Fatalf("failed to start first child process: %v", err) + } + + chldPipe := bufio.NewReader(chldStdout) + + 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 { + t.Fatalf("did not receive start from child, received %q, %v", got, err) + } +} + +func deathSignalParent() { + cmd := exec.Command(os.Args[0]) + cmd.Env = append(os.Environ(), + "GO_DEATHSIG_PARENT=", + "GO_DEATHSIG_CHILD=1", + ) + 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}, + } + cmd.SysProcAttr = &attrs + + err := cmd.Start() + if err != nil { + fmt.Fprintf(os.Stderr, "death signal parent error: %v\n", err) + os.Exit(1) + } + cmd.Wait() + os.Exit(0) +} + +func deathSignalChild() { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGUSR1) + go func() { + <-c + fmt.Println("ok") + os.Exit(0) + }() + fmt.Println("start") + + buf := make([]byte, 32) + os.Stdin.Read(buf) + + // We expected to be signaled before stdin closed + fmt.Println("not ok") + os.Exit(1) +} diff --git a/src/syscall/syscall_freebsd_test.go b/src/syscall/syscall_freebsd_test.go index 89c7959d0c43ac..72cd133b85dd0d 100644 --- a/src/syscall/syscall_freebsd_test.go +++ b/src/syscall/syscall_freebsd_test.go @@ -9,6 +9,7 @@ package syscall_test import ( "fmt" + "os" "syscall" "testing" "unsafe" @@ -53,3 +54,13 @@ func TestConvertFromDirent11(t *testing.T) { } } } + +func TestMain(m *testing.M) { + if os.Getenv("GO_DEATHSIG_PARENT") == "1" { + deathSignalParent() + } else if os.Getenv("GO_DEATHSIG_CHILD") == "1" { + deathSignalChild() + } + + os.Exit(m.Run()) +} diff --git a/src/syscall/syscall_linux_test.go b/src/syscall/syscall_linux_test.go index 442dc9f10eb87f..8d828be0153348 100644 --- a/src/syscall/syscall_linux_test.go +++ b/src/syscall/syscall_linux_test.go @@ -5,13 +5,11 @@ package syscall_test import ( - "bufio" "fmt" "io" "io/fs" "os" "os/exec" - "os/signal" "path/filepath" "runtime" "sort" @@ -19,7 +17,6 @@ import ( "strings" "syscall" "testing" - "time" "unsafe" ) @@ -153,120 +150,6 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func TestLinuxDeathSignal(t *testing.T) { - if os.Getuid() != 0 { - t.Skip("skipping root only test") - } - - // Copy the test binary to a location that a non-root user can read/execute - // after we drop privileges - tempDir, err := os.MkdirTemp("", "TestDeathSignal") - if err != nil { - t.Fatalf("cannot create temporary directory: %v", err) - } - defer os.RemoveAll(tempDir) - os.Chmod(tempDir, 0755) - - tmpBinary := filepath.Join(tempDir, filepath.Base(os.Args[0])) - - src, err := os.Open(os.Args[0]) - if err != nil { - t.Fatalf("cannot open binary %q, %v", os.Args[0], err) - } - defer src.Close() - - dst, err := os.OpenFile(tmpBinary, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) - if err != nil { - t.Fatalf("cannot create temporary binary %q, %v", tmpBinary, err) - } - if _, err := io.Copy(dst, src); err != nil { - t.Fatalf("failed to copy test binary to %q, %v", tmpBinary, err) - } - err = dst.Close() - if err != nil { - t.Fatalf("failed to close test binary %q, %v", tmpBinary, err) - } - - cmd := exec.Command(tmpBinary) - cmd.Env = append(os.Environ(), "GO_DEATHSIG_PARENT=1") - chldStdin, err := cmd.StdinPipe() - if err != nil { - t.Fatalf("failed to create new stdin pipe: %v", err) - } - chldStdout, err := cmd.StdoutPipe() - if err != nil { - t.Fatalf("failed to create new stdout pipe: %v", err) - } - cmd.Stderr = os.Stderr - - err = cmd.Start() - defer cmd.Wait() - if err != nil { - t.Fatalf("failed to start first child process: %v", err) - } - - chldPipe := bufio.NewReader(chldStdout) - - 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 { - t.Fatalf("did not receive start from child, received %q, %v", got, err) - } -} - -func deathSignalParent() { - cmd := exec.Command(os.Args[0]) - cmd.Env = append(os.Environ(), - "GO_DEATHSIG_PARENT=", - "GO_DEATHSIG_CHILD=1", - ) - 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}, - } - cmd.SysProcAttr = &attrs - - err := cmd.Start() - if err != nil { - fmt.Fprintf(os.Stderr, "death signal parent error: %v\n", err) - os.Exit(1) - } - cmd.Wait() - os.Exit(0) -} - -func deathSignalChild() { - c := make(chan os.Signal, 1) - signal.Notify(c, syscall.SIGUSR1) - go func() { - <-c - fmt.Println("ok") - os.Exit(0) - }() - fmt.Println("start") - - buf := make([]byte, 32) - os.Stdin.Read(buf) - - // We expected to be signaled before stdin closed - fmt.Println("not ok") - os.Exit(1) -} - func TestParseNetlinkMessage(t *testing.T) { for i, b := range [][]byte{ {103, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 2, 11, 0, 1, 0, 0, 0, 0, 5, 8, 0, 3,