Go version
go version go1.25.0 linux/amd64
Output of go env
in your module/workspace:
AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/agis/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/agis/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2541113073=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/home/agis/dev/x/go.mod'
GOMODCACHE='/home/agis/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/agis/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/agis/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.linux-amd64'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/agis/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/agis/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.linux-amd64/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.25.0'
GOWORK=''
PKG_CONFIG='pkg-config'
What did you do?
Brief
The way go tool
traps and forwards signals might cause tools that stay in the foreground to misbehave.
Note: I'm not sure if this is considered a bug or a "work as intended" situation. If there's intention of changing the behavior, I'd be happy to take a stab at it.
Description
go tool
traps signals and forwards them to the spawned tool: https://github.com/golang/go/blob/cdd8cf4988c7c0f2bb8eb795f74c4f803c63a70d/src/cmd/go/internal/tool/tool.go#L374-L383
However, if the spawned tool is a process that's in the foreground of the TTY, hitting Ctrl-C in the terminal will cause 2 SIGINT
signals to be delivered to the spawned tool: one from the kernel (the tool is in the same process group ID as go tool
) and one from go tool
itself.
This is problematic for tools that assign special meaning to a second SIGINT
. overmind (a process supervisor) is an example of this. The second signal causes it to forcefully kill all its processes.
Reproduction
// main.go
package main
func main() {}
// go.mod
module foo
go 1.25
toolchain go1.25.0
tool github.com/agis/gotool-sig
require github.com/agis/gotool-sig v0.0.0-20250826124522-e922fff2b3fb // indirect
Run go tool gotool-sig 1
and then hit Ctrl-C in the terminal. You'll get the following output:
$ go tool gotool-sig 1
PID: 176199, PPID: 176162, PGID: 176162
Signal monitor started with buffer size 1...
^CReceived signal: interrupt (2)
Received signal: interrupt (2)
You'll notice that 2 interrupt signals were delivered to the tool, instead of one.
What did you see happen?
N/A (see above)
What did you expect to see?
Only 1 SIGINT should be delivered.