Skip to content

Commit

Permalink
syscall: add support for SysProcAttr.Pdeathsig on FreeBSD
Browse files Browse the repository at this point in the history
Fixes golang#46258

Change-Id: I63f70e67274a9df39c757243b99b12e50a9e4784
  • Loading branch information
tklauser committed Oct 11, 2021
1 parent ec36339 commit 7323aac
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 141 deletions.
32 changes: 31 additions & 1 deletion src/syscall/exec_freebsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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, _ := rawSyscallNoError(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.
Expand Down Expand Up @@ -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, _ = rawSyscallNoError(SYS_GETPPID, 0, 0, 0)
if r1 != ppid {
pid, _ := rawSyscallNoError(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 {
Expand Down
2 changes: 1 addition & 1 deletion src/syscall/exec_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
157 changes: 157 additions & 0 deletions src/syscall/exec_pdeathsig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// 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 TestMain(m *testing.M) {
if os.Getenv("GO_DEATHSIG_PARENT") == "1" {
deathSignalParent()
} else if os.Getenv("GO_DEATHSIG_CHILD") == "1" {
deathSignalChild()
} else if os.Getenv("GO_SYSCALL_NOERROR") == "1" {
syscallNoError()
}

os.Exit(m.Run())
}

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)
}

func syscallNoError() {
// Test that the return value from SYS_GETEUID32 (which cannot fail)
// doesn't get treated as an error (see https://golang.org/issue/22924)
euid1, _, e := syscall.RawSyscall(syscall.Sys_GETEUID, 0, 0, 0)
euid2, _ := syscall.RawSyscallNoError(syscall.Sys_GETEUID, 0, 0, 0)

fmt.Println(uintptr(euid1), "/", int(e), "/", uintptr(euid2))
os.Exit(0)
}
139 changes: 0 additions & 139 deletions src/syscall/syscall_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,18 @@
package syscall_test

import (
"bufio"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"syscall"
"testing"
"time"
"unsafe"
)

Expand Down Expand Up @@ -141,132 +138,6 @@ func TestFchmodat(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()
} else if os.Getenv("GO_SYSCALL_NOERROR") == "1" {
syscallNoError()
}

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,
Expand Down Expand Up @@ -390,16 +261,6 @@ func filesystemIsNoSUID(path string) bool {
return st.Flags&syscall.MS_NOSUID != 0
}

func syscallNoError() {
// Test that the return value from SYS_GETEUID32 (which cannot fail)
// doesn't get treated as an error (see https://golang.org/issue/22924)
euid1, _, e := syscall.RawSyscall(syscall.Sys_GETEUID, 0, 0, 0)
euid2, _ := syscall.RawSyscallNoError(syscall.Sys_GETEUID, 0, 0, 0)

fmt.Println(uintptr(euid1), "/", int(e), "/", uintptr(euid2))
os.Exit(0)
}

// reference uapi/linux/prctl.h
const (
PR_GET_KEEPCAPS uintptr = 7
Expand Down

0 comments on commit 7323aac

Please sign in to comment.