mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
On unix if exec.Command() is given both ExtraFiles and Ctty, and the Ctty file descriptor overlaps the range of FDs intended for the child, then cmd.Start() the ioctl(fd,TIOCSCTTY) call fails with an "inappropriate ioctl for device" error. When child file descriptors overlap the new child's ctty the ctty will be closed in the fd shuffle before the TIOCSCTTY. Thus TIOCSCTTY is used on one of the ExtraFiles rather than the intended Ctty file. Thus the error. exec.Command() callers can workaround this by ensuring the Ctty fd is larger than any ExtraFiles destined for the child. Fix this by doing the ctty ioctl before the fd shuffle. Test for this issue by modifying TestTerminalSignal to use more ExtraFiles. The test fails on linux and freebsd without this change's syscall/*.go changes. Other platforms (e.g. darwin, aix, solaris) have the same fd shuffle logic, so the same fix is applied to them. However, I was only able to test on linux (32 and 64 bit) and freebsd (64 bit). Manual runs of the test in https://golang.org/issue/29458 start passing with this patch: Before: % /tmp/src/go/bin/go run t successfully ran child process with ParentExtraFileFdNum=5, ChildExtraFileFd=6, ParentPtyFd=7 panic: failed to run child process with ParentExtraFileFdNum=10, ChildExtraFileFd=11, ParentPtyFd=11: fork/exec /bin/true: inappropriate ioctl for device After: % /tmp/src/go/bin/go run t successfully ran child process with ParentExtraFileFdNum=5, ChildExtraFileFd=6, ParentPtyFd=7 successfully ran child process with ParentExtraFileFdNum=10, ChildExtraFileFd=11, ParentPtyFd=11 Fixes #29458 Change-Id: I99513de7b6073c7eb855f1eeb4d1f9dc0454ef8b Reviewed-on: https://go-review.googlesource.com/c/go/+/178919 Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Ian Lance Taylor <iant@golang.org>
250 lines
6.3 KiB
Go
250 lines
6.3 KiB
Go
// Copyright 2017 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.
|
|
|
|
// +build darwin dragonfly freebsd linux,!android netbsd openbsd
|
|
// +build cgo
|
|
|
|
// Note that this test does not work on Solaris: issue #22849.
|
|
// Don't run the test on Android because at least some versions of the
|
|
// C library do not define the posix_openpt function.
|
|
|
|
package signal_test
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal/internal/pty"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestTerminalSignal(t *testing.T) {
|
|
const enteringRead = "test program entering read"
|
|
if os.Getenv("GO_TEST_TERMINAL_SIGNALS") != "" {
|
|
var b [1]byte
|
|
fmt.Println(enteringRead)
|
|
n, err := os.Stdin.Read(b[:])
|
|
if n == 1 {
|
|
if b[0] == '\n' {
|
|
// This is what we expect
|
|
fmt.Println("read newline")
|
|
} else {
|
|
fmt.Printf("read 1 byte: %q\n", b)
|
|
}
|
|
} else {
|
|
fmt.Printf("read %d bytes\n", n)
|
|
}
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
// The test requires a shell that uses job control.
|
|
bash, err := exec.LookPath("bash")
|
|
if err != nil {
|
|
t.Skipf("could not find bash: %v", err)
|
|
}
|
|
|
|
scale := 1
|
|
if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" {
|
|
if sc, err := strconv.Atoi(s); err == nil {
|
|
scale = sc
|
|
}
|
|
}
|
|
pause := time.Duration(scale) * 10 * time.Millisecond
|
|
wait := time.Duration(scale) * 5 * time.Second
|
|
|
|
// The test only fails when using a "slow device," in this
|
|
// case a pseudo-terminal.
|
|
|
|
master, sname, err := pty.Open()
|
|
if err != nil {
|
|
ptyErr := err.(*pty.PtyError)
|
|
if ptyErr.FuncName == "posix_openpt" && ptyErr.Errno == syscall.EACCES {
|
|
t.Skip("posix_openpt failed with EACCES, assuming chroot and skipping")
|
|
}
|
|
t.Fatal(err)
|
|
}
|
|
defer master.Close()
|
|
slave, err := os.OpenFile(sname, os.O_RDWR, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer slave.Close()
|
|
|
|
// Start an interactive shell.
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, bash, "--norc", "--noprofile", "-i")
|
|
// Clear HISTFILE so that we don't read or clobber the user's bash history.
|
|
cmd.Env = append(os.Environ(), "HISTFILE=")
|
|
cmd.Stdin = slave
|
|
cmd.Stdout = slave
|
|
cmd.Stderr = slave
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
Setsid: true,
|
|
Setctty: true,
|
|
Ctty: int(slave.Fd()),
|
|
}
|
|
|
|
// Test ctty management by sending enough child fd to overlap the
|
|
// parent's fd intended for child's ctty.
|
|
for 2+len(cmd.ExtraFiles) < cmd.SysProcAttr.Ctty {
|
|
dummy, err := os.Open(os.DevNull)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer dummy.Close()
|
|
cmd.ExtraFiles = append(cmd.ExtraFiles, dummy)
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := slave.Close(); err != nil {
|
|
t.Errorf("closing slave: %v", err)
|
|
}
|
|
|
|
progReady := make(chan bool)
|
|
sawPrompt := make(chan bool, 10)
|
|
const prompt = "prompt> "
|
|
|
|
// Read data from master in the background.
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
defer wg.Wait()
|
|
go func() {
|
|
defer wg.Done()
|
|
input := bufio.NewReader(master)
|
|
var line, handled []byte
|
|
for {
|
|
b, err := input.ReadByte()
|
|
if err != nil {
|
|
if len(line) > 0 || len(handled) > 0 {
|
|
t.Logf("%q", append(handled, line...))
|
|
}
|
|
if perr, ok := err.(*os.PathError); ok {
|
|
err = perr.Err
|
|
}
|
|
// EOF means master is closed.
|
|
// EIO means child process is done.
|
|
// "file already closed" means deferred close of master has happened.
|
|
if err != io.EOF && err != syscall.EIO && !strings.Contains(err.Error(), "file already closed") {
|
|
t.Logf("error reading from master: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
line = append(line, b)
|
|
|
|
if b == '\n' {
|
|
t.Logf("%q", append(handled, line...))
|
|
line = nil
|
|
handled = nil
|
|
continue
|
|
}
|
|
|
|
if bytes.Contains(line, []byte(enteringRead)) {
|
|
close(progReady)
|
|
handled = append(handled, line...)
|
|
line = nil
|
|
} else if bytes.Contains(line, []byte(prompt)) && !bytes.Contains(line, []byte("PS1=")) {
|
|
sawPrompt <- true
|
|
handled = append(handled, line...)
|
|
line = nil
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Set the bash prompt so that we can see it.
|
|
if _, err := master.Write([]byte("PS1='" + prompt + "'\n")); err != nil {
|
|
t.Fatalf("setting prompt: %v", err)
|
|
}
|
|
select {
|
|
case <-sawPrompt:
|
|
case <-time.After(wait):
|
|
t.Fatal("timed out waiting for shell prompt")
|
|
}
|
|
|
|
// Start a small program that reads from stdin
|
|
// (namely the code at the top of this function).
|
|
if _, err := master.Write([]byte("GO_TEST_TERMINAL_SIGNALS=1 " + os.Args[0] + " -test.run=TestTerminalSignal\n")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Wait for the program to print that it is starting.
|
|
select {
|
|
case <-progReady:
|
|
case <-time.After(wait):
|
|
t.Fatal("timed out waiting for program to start")
|
|
}
|
|
|
|
// Give the program time to enter the read call.
|
|
// It doesn't matter much if we occasionally don't wait long enough;
|
|
// we won't be testing what we want to test, but the overall test
|
|
// will pass.
|
|
time.Sleep(pause)
|
|
|
|
// Send a ^Z to stop the program.
|
|
if _, err := master.Write([]byte{26}); err != nil {
|
|
t.Fatalf("writing ^Z to pty: %v", err)
|
|
}
|
|
|
|
// Wait for the program to stop and return to the shell.
|
|
select {
|
|
case <-sawPrompt:
|
|
case <-time.After(wait):
|
|
t.Fatal("timed out waiting for shell prompt")
|
|
}
|
|
|
|
// Restart the stopped program.
|
|
if _, err := master.Write([]byte("fg\n")); err != nil {
|
|
t.Fatalf("writing %q to pty: %v", "fg", err)
|
|
}
|
|
|
|
// Give the process time to restart.
|
|
// This is potentially racy: if the process does not restart
|
|
// quickly enough then the byte we send will go to bash rather
|
|
// than the program. Unfortunately there isn't anything we can
|
|
// look for to know that the program is running again.
|
|
// bash will print the program name, but that happens before it
|
|
// restarts the program.
|
|
time.Sleep(10 * pause)
|
|
|
|
// Write some data for the program to read,
|
|
// which should cause it to exit.
|
|
if _, err := master.Write([]byte{'\n'}); err != nil {
|
|
t.Fatalf("writing %q to pty: %v", "\n", err)
|
|
}
|
|
|
|
// Wait for the program to exit.
|
|
select {
|
|
case <-sawPrompt:
|
|
case <-time.After(wait):
|
|
t.Fatal("timed out waiting for shell prompt")
|
|
}
|
|
|
|
// Exit the shell with the program's exit status.
|
|
if _, err := master.Write([]byte("exit $?\n")); err != nil {
|
|
t.Fatalf("writing %q to pty: %v", "exit", err)
|
|
}
|
|
|
|
if err = cmd.Wait(); err != nil {
|
|
t.Errorf("subprogram failed: %v", err)
|
|
}
|
|
}
|