Skip to content

Commit

Permalink
syscall: Relocate Linux death signal code
Browse files Browse the repository at this point in the history
Fix a bug on Linux where using the `Pdeathsig` along with SETUID/SETGID
would cause the death signal to be ignored. This is because the
Linux kernel will clear the `deathsignal` field on a task when
performing a SETUID/SETGID system call.

To avoid this we simply move the `Pdeathsig` logic farther down in
the function after we have switched to our new uid/gid.

Fixes golang#9686

Change-Id: Id01896ad4e979b8c448e0061f00aa8762ca0ac94
  • Loading branch information
ajwdev committed Apr 21, 2015
1 parent 1b61a97 commit 7d0274f
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 20 deletions.
40 changes: 20 additions & 20 deletions src/syscall/exec_linux.go
Expand Up @@ -130,26 +130,6 @@ func forkAndExecInChild(argv0 *byte, argv, envv []*byte, chroot, dir *byte, attr
}
}

// Parent death signal
if sys.Pdeathsig != 0 {
_, _, err1 = RawSyscall6(SYS_PRCTL, PR_SET_PDEATHSIG, uintptr(sys.Pdeathsig), 0, 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
}
}
}

// Enable tracing if requested.
if sys.Ptrace {
_, _, err1 = RawSyscall(SYS_PTRACE, uintptr(PTRACE_TRACEME), 0, 0)
Expand Down Expand Up @@ -211,6 +191,26 @@ func forkAndExecInChild(argv0 *byte, argv, envv []*byte, chroot, dir *byte, attr
}
}

// Parent death signal
if sys.Pdeathsig != 0 {
_, _, err1 = RawSyscall6(SYS_PRCTL, PR_SET_PDEATHSIG, uintptr(sys.Pdeathsig), 0, 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 {
Expand Down
124 changes: 124 additions & 0 deletions src/syscall/syscall_unix_test.go
Expand Up @@ -7,14 +7,17 @@
package syscall_test

import (
"bufio"
"flag"
"fmt"
"io/ioutil"
"net"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"strings"
"syscall"
"testing"
"time"
Expand Down Expand Up @@ -58,6 +61,16 @@ func _() {
)
}

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

// TestFcntlFlock tests whether the file locking structure matches
// the calling convention of each kernel.
func TestFcntlFlock(t *testing.T) {
Expand Down Expand Up @@ -312,3 +325,114 @@ func TestSeekFailure(t *testing.T) {
t.Fatalf("Seek(-1, 0, 0) return error with empty message")
}
}

func TestLinuxDeathSignal(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("skipping linux only test")
}
if os.Getuid() != 0 {
t.Skip("skipping root only test")
}

// XXX The /tmp/go-buildNNNN directory that gets created for these tests
// has the access mode of 0700 which means that when we drop our privileges the
// new user will not be able to re-exec our test binary
baseSplit := strings.Split(os.Args[0], "/")[1:] // ignore leading slash
if len(baseSplit) < 2 {
t.Fatal("could not determine test directory")
}
testBaseDir := fmt.Sprintf("/%s/%s", baseSplit[0], baseSplit[1])

err := os.Chmod(testBaseDir, 0755)
if err != nil {
t.Fatalf("could not chmod test directory %q: %v", testBaseDir, err)
}
defer func() {
err = os.Chmod(testBaseDir, 0700)
if err != nil {
t.Fatalf("Could not re-chmod test directory %q: %v", testBaseDir, err)
}
}()

chldStdinR, chldStdinW, err := os.Pipe()
if err != nil {
t.Fatal("failed to create new stdin pipe: %v", err)
}
chldStdoutR, chldStdoutW, err := os.Pipe()
if err != nil {
t.Fatal("failed to create new stdout pipe: %v", err)
}
defer chldStdinW.Close()
defer chldStdoutR.Close()

cmd := exec.Command(os.Args[0])
cmd.Env = []string{"GO_DEATHSIG_PARENT=1"}
cmd.Stdin = chldStdinR
cmd.Stdout = chldStdoutW
cmd.Stderr = os.Stderr

err = cmd.Start()
if err != nil {
t.Fatalf("failed to start first child process: %v", err)
}
chldStdinR.Close()
chldStdoutW.Close()

chldPipe := bufio.NewReader(chldStdoutR)

if got, err := chldPipe.ReadString('\n'); got == "start\n" {
syscall.Kill(cmd.Process.Pid, syscall.SIGTERM)
cmd.Wait()

// Give grandchild a chance to deal with signal
time.Sleep(200 * time.Millisecond)

chldStdinW.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 = []string{"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")
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)
}

0 comments on commit 7d0274f

Please sign in to comment.