Source file src/os/signal/signal_cgo_test.go

     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  
    11  package signal_test
    12  
    13  import (
    14  	"context"
    15  	"encoding/binary"
    16  	"fmt"
    17  	"internal/testenv"
    18  	"internal/testpty"
    19  	"os"
    20  	"os/signal"
    21  	"runtime"
    22  	"strconv"
    23  	"syscall"
    24  	"testing"
    25  	"time"
    26  	"unsafe"
    27  )
    28  
    29  const (
    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.
    42  func 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.
   232  func 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  		_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, ptyFD, syscall.TIOCSPGRP, uintptr(unsafe.Pointer(&pgrp)))
   308  		if errno != 0 {
   309  			return fmt.Errorf("error setting tty process group: %w", errno)
   310  		}
   311  
   312  		// Give the kernel time to potentially wake readers and have
   313  		// them return EINTR (darwin does this).
   314  		time.Sleep(pause)
   315  
   316  		// Give TTY back.
   317  		pid := int32(cmd.Process.Pid) // assume that pid_t is int32
   318  		_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, ptyFD, syscall.TIOCSPGRP, uintptr(unsafe.Pointer(&pid)))
   319  		if errno != 0 {
   320  			return fmt.Errorf("error setting tty process group back: %w", errno)
   321  		}
   322  
   323  		// Report that we are done and SIGCONT can be sent. Note that
   324  		// the actual byte we send doesn't matter.
   325  		if _, err := controlW.Write(b[:1]); err != nil {
   326  			return fmt.Errorf("error writing readiness: %w", err)
   327  		}
   328  
   329  		return nil
   330  	}
   331  
   332  	err := fn()
   333  	if err != nil {
   334  		fmt.Fprintf(os.Stderr, "session leader error: %v\n", err)
   335  		cmd.Process.Kill()
   336  		// Wait for exit below.
   337  	}
   338  
   339  	werr := cmd.Wait()
   340  	if werr != nil {
   341  		fmt.Fprintf(os.Stderr, "error running second subprocess: %v\n", err)
   342  	}
   343  
   344  	if err != nil || werr != nil {
   345  		os.Exit(1)
   346  	}
   347  
   348  	os.Exit(0)
   349  }
   350  
   351  // GO_TEST_TERMINAL_SIGNALS=2 subprocess above.
   352  func runStoppingChild() {
   353  	pty := os.NewFile(ptyFD, "pty")
   354  
   355  	var b [1]byte
   356  	if _, err := pty.Write(b[:]); err != nil {
   357  		fmt.Fprintf(os.Stderr, "error writing byte to PTY: %v\n", err)
   358  		os.Exit(1)
   359  	}
   360  
   361  	_, err := pty.Read(b[:])
   362  	if err != nil {
   363  		fmt.Fprintln(os.Stderr, err)
   364  		os.Exit(1)
   365  	}
   366  	if b[0] == '\n' {
   367  		// This is what we expect
   368  		fmt.Println("read newline")
   369  	} else {
   370  		fmt.Fprintf(os.Stderr, "read 1 unexpected byte: %q\n", b)
   371  		os.Exit(1)
   372  	}
   373  	os.Exit(0)
   374  }
   375  

View as plain text