1// Copyright 2017 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 (darwin || dragonfly || freebsd || (linux && !android) || netbsd || openbsd) && cgo
6
7// Note that this test does not work on Solaris: issue #22849.
8// Don't run the test on Android because at least some versions of the
9// C library do not define the posix_openpt function.
10
11package signal_test
12
13import (
14	"context"
15	"encoding/binary"
16	"fmt"
17	"internal/syscall/unix"
18	"internal/testenv"
19	"internal/testpty"
20	"os"
21	"os/signal"
22	"runtime"
23	"strconv"
24	"syscall"
25	"testing"
26	"time"
27)
28
29const (
30	ptyFD     = 3 // child end of pty.
31	controlFD = 4 // child end of control pipe.
32)
33
34// TestTerminalSignal tests that read from a pseudo-terminal does not return an
35// error if the process is SIGSTOP'd and put in the background during the read.
36//
37// This test simulates stopping a Go process running in a shell with ^Z and
38// then resuming with `fg`.
39//
40// This is a regression test for https://go.dev/issue/22838. On Darwin, PTY
41// reads return EINTR when this occurs, and Go should automatically retry.
42func TestTerminalSignal(t *testing.T) {
43	// This test simulates stopping a Go process running in a shell with ^Z
44	// and then resuming with `fg`. This sounds simple, but is actually
45	// quite complicated.
46	//
47	// In principle, what we are doing is:
48	// 1. Creating a new PTY parent/child FD pair.
49	// 2. Create a child that is in the foreground process group of the PTY, and read() from that process.
50	// 3. Stop the child with ^Z.
51	// 4. Take over as foreground process group of the PTY from the parent.
52	// 5. Make the child foreground process group again.
53	// 6. Continue the child.
54	//
55	// On Darwin, step 4 results in the read() returning EINTR once the
56	// process continues. internal/poll should automatically retry the
57	// read.
58	//
59	// These steps are complicated by the rules around foreground process
60	// groups. A process group cannot be foreground if it is "orphaned",
61	// unless it masks SIGTTOU.  i.e., to be foreground the process group
62	// must have a parent process group in the same session or mask SIGTTOU
63	// (which we do). An orphaned process group cannot receive
64	// terminal-generated SIGTSTP at all.
65	//
66	// Achieving this requires three processes total:
67	// - Top-level process: this is the main test process and creates the
68	// pseudo-terminal.
69	// - GO_TEST_TERMINAL_SIGNALS=1: This process creates a new process
70	// group and session. The PTY is the controlling terminal for this
71	// session. This process masks SIGTTOU, making it eligible to be a
72	// foreground process group. This process will take over as foreground
73	// from subprocess 2 (step 4 above).
74	// - GO_TEST_TERMINAL_SIGNALS=2: This process create a child process
75	// group of subprocess 1, and is the original foreground process group
76	// for the PTY. This subprocess is the one that is SIGSTOP'd.
77
78	if runtime.GOOS == "dragonfly" {
79		t.Skip("skipping: wait hangs on dragonfly; see https://go.dev/issue/56132")
80	}
81
82	scale := 1
83	if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" {
84		if sc, err := strconv.Atoi(s); err == nil {
85			scale = sc
86		}
87	}
88	pause := time.Duration(scale) * 10 * time.Millisecond
89
90	lvl := os.Getenv("GO_TEST_TERMINAL_SIGNALS")
91	switch lvl {
92	case "":
93		// Main test process, run code below.
94		break
95	case "1":
96		runSessionLeader(t, pause)
97		panic("unreachable")
98	case "2":
99		runStoppingChild()
100		panic("unreachable")
101	default:
102		fmt.Fprintf(os.Stderr, "unknown subprocess level %s\n", lvl)
103		os.Exit(1)
104	}
105
106	t.Parallel()
107
108	pty, procTTYName, err := testpty.Open()
109	if err != nil {
110		ptyErr := err.(*testpty.PtyError)
111		if ptyErr.FuncName == "posix_openpt" && ptyErr.Errno == syscall.EACCES {
112			t.Skip("posix_openpt failed with EACCES, assuming chroot and skipping")
113		}
114		t.Fatal(err)
115	}
116	defer pty.Close()
117	procTTY, err := os.OpenFile(procTTYName, os.O_RDWR, 0)
118	if err != nil {
119		t.Fatal(err)
120	}
121	defer procTTY.Close()
122
123	// Control pipe. GO_TEST_TERMINAL_SIGNALS=2 send the PID of
124	// GO_TEST_TERMINAL_SIGNALS=3 here. After SIGSTOP, it also writes a
125	// byte to indicate that the foreground cycling is complete.
126	controlR, controlW, err := os.Pipe()
127	if err != nil {
128		t.Fatal(err)
129	}
130
131	var (
132		ctx     = context.Background()
133		cmdArgs = []string{"-test.run=^TestTerminalSignal$"}
134	)
135	if deadline, ok := t.Deadline(); ok {
136		d := time.Until(deadline)
137		var cancel context.CancelFunc
138		ctx, cancel = context.WithTimeout(ctx, d)
139		t.Cleanup(cancel)
140
141		// We run the subprocess with an additional 20% margin to allow it to fail
142		// and clean up gracefully if it times out.
143		cmdArgs = append(cmdArgs, fmt.Sprintf("-test.timeout=%v", d*5/4))
144	}
145
146	cmd := testenv.CommandContext(t, ctx, os.Args[0], cmdArgs...)
147	cmd.Env = append(os.Environ(), "GO_TEST_TERMINAL_SIGNALS=1")
148	cmd.Stdin = os.Stdin
149	cmd.Stdout = os.Stdout // for logging
150	cmd.Stderr = os.Stderr
151	cmd.ExtraFiles = []*os.File{procTTY, controlW}
152	cmd.SysProcAttr = &syscall.SysProcAttr{
153		Setsid:  true,
154		Setctty: true,
155		Ctty:    ptyFD,
156	}
157
158	if err := cmd.Start(); err != nil {
159		t.Fatal(err)
160	}
161
162	if err := procTTY.Close(); err != nil {
163		t.Errorf("closing procTTY: %v", err)
164	}
165
166	if err := controlW.Close(); err != nil {
167		t.Errorf("closing controlW: %v", err)
168	}
169
170	// Wait for first child to send the second child's PID.
171	b := make([]byte, 8)
172	n, err := controlR.Read(b)
173	if err != nil {
174		t.Fatalf("error reading child pid: %v\n", err)
175	}
176	if n != 8 {
177		t.Fatalf("unexpected short read n = %d\n", n)
178	}
179	pid := binary.LittleEndian.Uint64(b[:])
180	process, err := os.FindProcess(int(pid))
181	if err != nil {
182		t.Fatalf("unable to find child process: %v", err)
183	}
184
185	// Wait for the third child to write a byte indicating that it is
186	// entering the read.
187	b = make([]byte, 1)
188	_, err = pty.Read(b)
189	if err != nil {
190		t.Fatalf("error reading from child: %v", err)
191	}
192
193	// Give the program time to enter the read call.
194	// It doesn't matter much if we occasionally don't wait long enough;
195	// we won't be testing what we want to test, but the overall test
196	// will pass.
197	time.Sleep(pause)
198
199	t.Logf("Sending ^Z...")
200
201	// Send a ^Z to stop the program.
202	if _, err := pty.Write([]byte{26}); err != nil {
203		t.Fatalf("writing ^Z to pty: %v", err)
204	}
205
206	// Wait for subprocess 1 to cycle the foreground process group.
207	if _, err := controlR.Read(b); err != nil {
208		t.Fatalf("error reading readiness: %v", err)
209	}
210
211	t.Logf("Sending SIGCONT...")
212
213	// Restart the stopped program.
214	if err := process.Signal(syscall.SIGCONT); err != nil {
215		t.Fatalf("Signal(SIGCONT) got err %v want nil", err)
216	}
217
218	// Write some data for the program to read, which should cause it to
219	// exit.
220	if _, err := pty.Write([]byte{'\n'}); err != nil {
221		t.Fatalf("writing %q to pty: %v", "\n", err)
222	}
223
224	t.Logf("Waiting for exit...")
225
226	if err = cmd.Wait(); err != nil {
227		t.Errorf("subprogram failed: %v", err)
228	}
229}
230
231// GO_TEST_TERMINAL_SIGNALS=1 subprocess above.
232func runSessionLeader(t *testing.T, pause time.Duration) {
233	// "Attempts to use tcsetpgrp() from a process which is a
234	// member of a background process group on a fildes associated
235	// with its controlling terminal shall cause the process group
236	// to be sent a SIGTTOU signal. If the calling thread is
237	// blocking SIGTTOU signals or the process is ignoring SIGTTOU
238	// signals, the process shall be allowed to perform the
239	// operation, and no signal is sent."
240	//  -https://pubs.opengroup.org/onlinepubs/9699919799/functions/tcsetpgrp.html
241	//
242	// We are changing the terminal to put us in the foreground, so
243	// we must ignore SIGTTOU. We are also an orphaned process
244	// group (see above), so we must mask SIGTTOU to be eligible to
245	// become foreground at all.
246	signal.Ignore(syscall.SIGTTOU)
247
248	pty := os.NewFile(ptyFD, "pty")
249	controlW := os.NewFile(controlFD, "control-pipe")
250
251	var (
252		ctx     = context.Background()
253		cmdArgs = []string{"-test.run=^TestTerminalSignal$"}
254	)
255	if deadline, ok := t.Deadline(); ok {
256		d := time.Until(deadline)
257		var cancel context.CancelFunc
258		ctx, cancel = context.WithTimeout(ctx, d)
259		t.Cleanup(cancel)
260
261		// We run the subprocess with an additional 20% margin to allow it to fail
262		// and clean up gracefully if it times out.
263		cmdArgs = append(cmdArgs, fmt.Sprintf("-test.timeout=%v", d*5/4))
264	}
265
266	cmd := testenv.CommandContext(t, ctx, os.Args[0], cmdArgs...)
267	cmd.Env = append(os.Environ(), "GO_TEST_TERMINAL_SIGNALS=2")
268	cmd.Stdin = os.Stdin
269	cmd.Stdout = os.Stdout
270	cmd.Stderr = os.Stderr
271	cmd.ExtraFiles = []*os.File{pty}
272	cmd.SysProcAttr = &syscall.SysProcAttr{
273		Foreground: true,
274		Ctty:       ptyFD,
275	}
276	if err := cmd.Start(); err != nil {
277		fmt.Fprintf(os.Stderr, "error starting second subprocess: %v\n", err)
278		os.Exit(1)
279	}
280
281	fn := func() error {
282		var b [8]byte
283		binary.LittleEndian.PutUint64(b[:], uint64(cmd.Process.Pid))
284		_, err := controlW.Write(b[:])
285		if err != nil {
286			return fmt.Errorf("error writing child pid: %w", err)
287		}
288
289		// Wait for stop.
290		var status syscall.WaitStatus
291		for {
292			_, err = syscall.Wait4(cmd.Process.Pid, &status, syscall.WUNTRACED, nil)
293			if err != syscall.EINTR {
294				break
295			}
296		}
297		if err != nil {
298			return fmt.Errorf("error waiting for stop: %w", err)
299		}
300
301		if !status.Stopped() {
302			return fmt.Errorf("unexpected wait status: %v", status)
303		}
304
305		// Take TTY.
306		pgrp := int32(syscall.Getpgrp()) // assume that pid_t is int32
307		if err := unix.Tcsetpgrp(ptyFD, pgrp); err != nil {
308			return fmt.Errorf("error setting tty process group: %w", err)
309		}
310
311		// Give the kernel time to potentially wake readers and have
312		// them return EINTR (darwin does this).
313		time.Sleep(pause)
314
315		// Give TTY back.
316		pid := int32(cmd.Process.Pid) // assume that pid_t is int32
317		if err := unix.Tcsetpgrp(ptyFD, pid); err != nil {
318			return fmt.Errorf("error setting tty process group back: %w", err)
319		}
320
321		// Report that we are done and SIGCONT can be sent. Note that
322		// the actual byte we send doesn't matter.
323		if _, err := controlW.Write(b[:1]); err != nil {
324			return fmt.Errorf("error writing readiness: %w", err)
325		}
326
327		return nil
328	}
329
330	err := fn()
331	if err != nil {
332		fmt.Fprintf(os.Stderr, "session leader error: %v\n", err)
333		cmd.Process.Kill()
334		// Wait for exit below.
335	}
336
337	werr := cmd.Wait()
338	if werr != nil {
339		fmt.Fprintf(os.Stderr, "error running second subprocess: %v\n", err)
340	}
341
342	if err != nil || werr != nil {
343		os.Exit(1)
344	}
345
346	os.Exit(0)
347}
348
349// GO_TEST_TERMINAL_SIGNALS=2 subprocess above.
350func runStoppingChild() {
351	pty := os.NewFile(ptyFD, "pty")
352
353	var b [1]byte
354	if _, err := pty.Write(b[:]); err != nil {
355		fmt.Fprintf(os.Stderr, "error writing byte to PTY: %v\n", err)
356		os.Exit(1)
357	}
358
359	_, err := pty.Read(b[:])
360	if err != nil {
361		fmt.Fprintln(os.Stderr, err)
362		os.Exit(1)
363	}
364	if b[0] == '\n' {
365		// This is what we expect
366		fmt.Println("read newline")
367	} else {
368		fmt.Fprintf(os.Stderr, "read 1 unexpected byte: %q\n", b)
369		os.Exit(1)
370	}
371	os.Exit(0)
372}
373