Skip to content

os/exec: (*SysProcAttr).Foreground causes the spawned process to hang #37217

@miquella

Description

@miquella

What version of Go are you using (go version)?

$ go version
go version go1.13.6 linux/amd64

Does this issue reproduce with the latest release?

Yes — tried with go1.14rc1

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/markse/.cache/go-build"
GOENV="/home/markse/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/markse"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build088413182=/tmp/go-build -gno-record-gcc-switches"

What did you do?

package main

import (
    "os"
    "os/exec"
    // "os/signal"
    "syscall"
)

func main() {
    // This does make it work, but shouldn't be necessary and causes side-effects
    // signal.Ignore(syscall.SIGTTIN, syscall.SIGTTOU)

    cmd := exec.Command("echo", "hello")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Foreground: true,
    }
    cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
    cmd.Run()
}

What did you expect to see?

The process should execute echo and display hello.

What did you see instead?

The process hangs.

ps shows the child process as suspended and doesn't appear to have called execve yet:

$ ps j
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND

 6072 25553 25553  6072 pts/0    25553 Dl+   1000   0:00 ./foreground
25553 25558 25558  6072 pts/0    25553 T     1000   0:00 ./foreground

Explanation

From what we can determine, we believe this is what is happening:

  1. The process is forked (ref)
  2. The child calls setpgid/SYS_SETPGID, creating a new background process group (ref)
  3. The child calls tcsetpgrp/TIOCSPGRP, causing the process to be suspended (ref)

While a process in the foreground process group is allowed to call tcsetpgrp, a process in a background process group has an additional hurdle. From the man page for tcsetpgrp:

If tcsetpgrp() is called by a member of a background process group in its session, and the calling process is not blocking or ignoring SIGTTOU, a SIGTTOU signal is sent to all members of this background process group.

Which explains why the process is getting suspended, as the default action for SIGTTOU is to stop the process. This also explains when ignoring SIGTTOU before spawning the process fixes the problem. However, this has an undesired side-effect: the subsequently exec'd process then ignores SIGTTOU as well.

Conclusion

Unfortunately, because all of this work is done in the child fork, there is no way for Go user code to reset the ignored SIGTTOU before exec'ing the process.

So there is no way to have a user ignore the SIGTTOU signal without an adverse effect on the child process.

Proposal: if the SIGTTOU signal could be ignored for the duration of the tcsetpgrp/TIOCSPGRP call and restored to its prior state immediately following, we believe this would elicit the expected behavior without adverse side-effects.

Metadata

Metadata

Assignees

No one assigned

    Labels

    FrozenDueToAgeNeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions