1// Copyright 2015 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5//go:build freebsd || linux
6
7package syscall_test
8
9import (
10	"bufio"
11	"fmt"
12	"internal/testenv"
13	"io"
14	"os"
15	"os/exec"
16	"os/signal"
17	"os/user"
18	"path/filepath"
19	"strconv"
20	"strings"
21	"syscall"
22	"testing"
23)
24
25// TestDeathSignalSetuid verifies that a command run with a different UID still
26// receives PDeathsig; it is a regression test for https://go.dev/issue/9686.
27func TestDeathSignalSetuid(t *testing.T) {
28	if testing.Short() {
29		t.Skipf("skipping test that copies its binary into temp dir")
30	}
31
32	// Copy the test binary to a location that another user can read/execute
33	// after we drop privileges.
34	//
35	// TODO(bcmills): Why do we believe that another users will be able to
36	// execute a binary in this directory? (It could be mounted noexec.)
37	tempDir, err := os.MkdirTemp("", "TestDeathSignal")
38	if err != nil {
39		t.Fatalf("cannot create temporary directory: %v", err)
40	}
41	defer os.RemoveAll(tempDir)
42	os.Chmod(tempDir, 0755)
43
44	tmpBinary := filepath.Join(tempDir, filepath.Base(os.Args[0]))
45
46	src, err := os.Open(os.Args[0])
47	if err != nil {
48		t.Fatalf("cannot open binary %q, %v", os.Args[0], err)
49	}
50	defer src.Close()
51
52	dst, err := os.OpenFile(tmpBinary, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
53	if err != nil {
54		t.Fatalf("cannot create temporary binary %q, %v", tmpBinary, err)
55	}
56	if _, err := io.Copy(dst, src); err != nil {
57		t.Fatalf("failed to copy test binary to %q, %v", tmpBinary, err)
58	}
59	err = dst.Close()
60	if err != nil {
61		t.Fatalf("failed to close test binary %q, %v", tmpBinary, err)
62	}
63
64	cmd := testenv.Command(t, tmpBinary)
65	cmd.Env = append(cmd.Environ(), "GO_DEATHSIG_PARENT=1")
66	chldStdin, err := cmd.StdinPipe()
67	if err != nil {
68		t.Fatalf("failed to create new stdin pipe: %v", err)
69	}
70	chldStdout, err := cmd.StdoutPipe()
71	if err != nil {
72		t.Fatalf("failed to create new stdout pipe: %v", err)
73	}
74	stderr := new(strings.Builder)
75	cmd.Stderr = stderr
76
77	err = cmd.Start()
78	defer func() {
79		chldStdin.Close()
80		cmd.Wait()
81		if stderr.Len() > 0 {
82			t.Logf("stderr:\n%s", stderr)
83		}
84	}()
85	if err != nil {
86		t.Fatalf("failed to start first child process: %v", err)
87	}
88
89	chldPipe := bufio.NewReader(chldStdout)
90
91	if got, err := chldPipe.ReadString('\n'); got == "start\n" {
92		syscall.Kill(cmd.Process.Pid, syscall.SIGTERM)
93
94		want := "ok\n"
95		if got, err = chldPipe.ReadString('\n'); got != want {
96			t.Fatalf("expected %q, received %q, %v", want, got, err)
97		}
98	} else if got == "skip\n" {
99		t.Skipf("skipping: parent could not run child program as selected user")
100	} else {
101		t.Fatalf("did not receive start from child, received %q, %v", got, err)
102	}
103}
104
105func deathSignalParent() {
106	var (
107		u   *user.User
108		err error
109	)
110	if os.Getuid() == 0 {
111		tryUsers := []string{"nobody"}
112		if testenv.Builder() != "" {
113			tryUsers = append(tryUsers, "gopher")
114		}
115		for _, name := range tryUsers {
116			u, err = user.Lookup(name)
117			if err == nil {
118				break
119			}
120			fmt.Fprintf(os.Stderr, "Lookup(%q): %v\n", name, err)
121		}
122	}
123	if u == nil {
124		// If we couldn't find an unprivileged user to run as, try running as
125		// the current user. (Empirically this still causes the call to Start to
126		// fail with a permission error if running as a non-root user on Linux.)
127		u, err = user.Current()
128		if err != nil {
129			fmt.Fprintln(os.Stderr, err)
130			os.Exit(1)
131		}
132	}
133
134	uid, err := strconv.ParseUint(u.Uid, 10, 32)
135	if err != nil {
136		fmt.Fprintf(os.Stderr, "invalid UID: %v\n", err)
137		os.Exit(1)
138	}
139	gid, err := strconv.ParseUint(u.Gid, 10, 32)
140	if err != nil {
141		fmt.Fprintf(os.Stderr, "invalid GID: %v\n", err)
142		os.Exit(1)
143	}
144
145	cmd := exec.Command(os.Args[0])
146	cmd.Env = append(os.Environ(),
147		"GO_DEATHSIG_PARENT=",
148		"GO_DEATHSIG_CHILD=1",
149	)
150	cmd.Stdin = os.Stdin
151	cmd.Stdout = os.Stdout
152	attrs := syscall.SysProcAttr{
153		Pdeathsig:  syscall.SIGUSR1,
154		Credential: &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)},
155	}
156	cmd.SysProcAttr = &attrs
157
158	fmt.Fprintf(os.Stderr, "starting process as user %q\n", u.Username)
159	if err := cmd.Start(); err != nil {
160		fmt.Fprintln(os.Stderr, err)
161		if testenv.SyscallIsNotSupported(err) {
162			fmt.Println("skip")
163			os.Exit(0)
164		}
165		os.Exit(1)
166	}
167	cmd.Wait()
168	os.Exit(0)
169}
170
171func deathSignalChild() {
172	c := make(chan os.Signal, 1)
173	signal.Notify(c, syscall.SIGUSR1)
174	go func() {
175		<-c
176		fmt.Println("ok")
177		os.Exit(0)
178	}()
179	fmt.Println("start")
180
181	buf := make([]byte, 32)
182	os.Stdin.Read(buf)
183
184	// We expected to be signaled before stdin closed
185	fmt.Println("not ok")
186	os.Exit(1)
187}
188