mirror of
https://github.com/golang/go
synced 2024-11-13 20:20:30 -07:00
715ba65563
If the test hangs due to a deadlock in a subprocess, we want a goroutine dump of that process to figure out the nature of the deadlock. SIGQUIT causes the Go runtime to produce exactly such a dump (unless the runtime itself is badly deadlocked). For #52998. Change-Id: Id9b3ba89d8f705e14f6cd789353fc2b7f4774ad3 Reviewed-on: https://go-review.googlesource.com/c/go/+/407954 Reviewed-by: Ian Lance Taylor <iant@google.com> Run-TryBot: Bryan Mills <bcmills@google.com> Auto-Submit: Bryan Mills <bcmills@google.com> TryBot-Result: Gopher Robot <gobot@golang.org>
519 lines
12 KiB
Go
519 lines
12 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.
|
|
|
|
// sanitizers_test checks the use of Go with sanitizers like msan, asan, etc.
|
|
// See https://github.com/google/sanitizers.
|
|
package sanitizers_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
"unicode"
|
|
)
|
|
|
|
var overcommit struct {
|
|
sync.Once
|
|
value int
|
|
err error
|
|
}
|
|
|
|
// requireOvercommit skips t if the kernel does not allow overcommit.
|
|
func requireOvercommit(t *testing.T) {
|
|
t.Helper()
|
|
|
|
overcommit.Once.Do(func() {
|
|
var out []byte
|
|
out, overcommit.err = os.ReadFile("/proc/sys/vm/overcommit_memory")
|
|
if overcommit.err != nil {
|
|
return
|
|
}
|
|
overcommit.value, overcommit.err = strconv.Atoi(string(bytes.TrimSpace(out)))
|
|
})
|
|
|
|
if overcommit.err != nil {
|
|
t.Skipf("couldn't determine vm.overcommit_memory (%v); assuming no overcommit", overcommit.err)
|
|
}
|
|
if overcommit.value == 2 {
|
|
t.Skip("vm.overcommit_memory=2")
|
|
}
|
|
}
|
|
|
|
var env struct {
|
|
sync.Once
|
|
m map[string]string
|
|
err error
|
|
}
|
|
|
|
// goEnv returns the output of $(go env) as a map.
|
|
func goEnv(key string) (string, error) {
|
|
env.Once.Do(func() {
|
|
var out []byte
|
|
out, env.err = exec.Command("go", "env", "-json").Output()
|
|
if env.err != nil {
|
|
return
|
|
}
|
|
|
|
env.m = make(map[string]string)
|
|
env.err = json.Unmarshal(out, &env.m)
|
|
})
|
|
if env.err != nil {
|
|
return "", env.err
|
|
}
|
|
|
|
v, ok := env.m[key]
|
|
if !ok {
|
|
return "", fmt.Errorf("`go env`: no entry for %v", key)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// replaceEnv sets the key environment variable to value in cmd.
|
|
func replaceEnv(cmd *exec.Cmd, key, value string) {
|
|
if cmd.Env == nil {
|
|
cmd.Env = os.Environ()
|
|
}
|
|
cmd.Env = append(cmd.Env, key+"="+value)
|
|
}
|
|
|
|
// mustRun executes t and fails cmd with a well-formatted message if it fails.
|
|
func mustRun(t *testing.T, cmd *exec.Cmd) {
|
|
t.Helper()
|
|
out := new(strings.Builder)
|
|
cmd.Stdout = out
|
|
cmd.Stderr = out
|
|
|
|
err := cmd.Start()
|
|
if err != nil {
|
|
t.Fatalf("%v: %v", cmd, err)
|
|
}
|
|
|
|
if deadline, ok := t.Deadline(); ok {
|
|
timeout := time.Until(deadline)
|
|
timeout -= timeout / 10 // Leave 10% headroom for logging and cleanup.
|
|
timer := time.AfterFunc(timeout, func() {
|
|
cmd.Process.Signal(syscall.SIGQUIT)
|
|
})
|
|
defer timer.Stop()
|
|
}
|
|
|
|
if err := cmd.Wait(); err != nil {
|
|
t.Fatalf("%v exited with %v\n%s", cmd, err, out)
|
|
}
|
|
}
|
|
|
|
// cc returns a cmd that executes `$(go env CC) $(go env GOGCCFLAGS) $args`.
|
|
func cc(args ...string) (*exec.Cmd, error) {
|
|
CC, err := goEnv("CC")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
GOGCCFLAGS, err := goEnv("GOGCCFLAGS")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Split GOGCCFLAGS, respecting quoting.
|
|
//
|
|
// TODO(bcmills): This code also appears in
|
|
// misc/cgo/testcarchive/carchive_test.go, and perhaps ought to go in
|
|
// src/cmd/dist/test.go as well. Figure out where to put it so that it can be
|
|
// shared.
|
|
var flags []string
|
|
quote := '\000'
|
|
start := 0
|
|
lastSpace := true
|
|
backslash := false
|
|
for i, c := range GOGCCFLAGS {
|
|
if quote == '\000' && unicode.IsSpace(c) {
|
|
if !lastSpace {
|
|
flags = append(flags, GOGCCFLAGS[start:i])
|
|
lastSpace = true
|
|
}
|
|
} else {
|
|
if lastSpace {
|
|
start = i
|
|
lastSpace = false
|
|
}
|
|
if quote == '\000' && !backslash && (c == '"' || c == '\'') {
|
|
quote = c
|
|
backslash = false
|
|
} else if !backslash && quote == c {
|
|
quote = '\000'
|
|
} else if (quote == '\000' || quote == '"') && !backslash && c == '\\' {
|
|
backslash = true
|
|
} else {
|
|
backslash = false
|
|
}
|
|
}
|
|
}
|
|
if !lastSpace {
|
|
flags = append(flags, GOGCCFLAGS[start:])
|
|
}
|
|
|
|
cmd := exec.Command(CC, flags...)
|
|
cmd.Args = append(cmd.Args, args...)
|
|
return cmd, nil
|
|
}
|
|
|
|
type version struct {
|
|
name string
|
|
major, minor int
|
|
}
|
|
|
|
var compiler struct {
|
|
sync.Once
|
|
version
|
|
err error
|
|
}
|
|
|
|
// compilerVersion detects the version of $(go env CC).
|
|
//
|
|
// It returns a non-nil error if the compiler matches a known version schema but
|
|
// the version could not be parsed, or if $(go env CC) could not be determined.
|
|
func compilerVersion() (version, error) {
|
|
compiler.Once.Do(func() {
|
|
compiler.err = func() error {
|
|
compiler.name = "unknown"
|
|
|
|
cmd, err := cc("--version")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
// Compiler does not support "--version" flag: not Clang or GCC.
|
|
return nil
|
|
}
|
|
|
|
var match [][]byte
|
|
if bytes.HasPrefix(out, []byte("gcc")) {
|
|
compiler.name = "gcc"
|
|
cmd, err := cc("-v")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
// gcc, but does not support gcc's "-v" flag?!
|
|
return err
|
|
}
|
|
gccRE := regexp.MustCompile(`gcc version (\d+)\.(\d+)`)
|
|
match = gccRE.FindSubmatch(out)
|
|
} else {
|
|
clangRE := regexp.MustCompile(`clang version (\d+)\.(\d+)`)
|
|
if match = clangRE.FindSubmatch(out); len(match) > 0 {
|
|
compiler.name = "clang"
|
|
}
|
|
}
|
|
|
|
if len(match) < 3 {
|
|
return nil // "unknown"
|
|
}
|
|
if compiler.major, err = strconv.Atoi(string(match[1])); err != nil {
|
|
return err
|
|
}
|
|
if compiler.minor, err = strconv.Atoi(string(match[2])); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}()
|
|
})
|
|
return compiler.version, compiler.err
|
|
}
|
|
|
|
// compilerSupportsLocation reports whether the compiler should be
|
|
// able to provide file/line information in backtraces.
|
|
func compilerSupportsLocation() bool {
|
|
compiler, err := compilerVersion()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
switch compiler.name {
|
|
case "gcc":
|
|
return compiler.major >= 10
|
|
case "clang":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// compilerRequiredAsanVersion reports whether the compiler is the version required by Asan.
|
|
func compilerRequiredAsanVersion() bool {
|
|
compiler, err := compilerVersion()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
switch compiler.name {
|
|
case "gcc":
|
|
return compiler.major >= 7
|
|
case "clang":
|
|
return compiler.major >= 9
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
type compilerCheck struct {
|
|
once sync.Once
|
|
err error
|
|
skip bool // If true, skip with err instead of failing with it.
|
|
}
|
|
|
|
type config struct {
|
|
sanitizer string
|
|
|
|
cFlags, ldFlags, goFlags []string
|
|
|
|
sanitizerCheck, runtimeCheck compilerCheck
|
|
}
|
|
|
|
var configs struct {
|
|
sync.Mutex
|
|
m map[string]*config
|
|
}
|
|
|
|
// configure returns the configuration for the given sanitizer.
|
|
func configure(sanitizer string) *config {
|
|
configs.Lock()
|
|
defer configs.Unlock()
|
|
if c, ok := configs.m[sanitizer]; ok {
|
|
return c
|
|
}
|
|
|
|
c := &config{
|
|
sanitizer: sanitizer,
|
|
cFlags: []string{"-fsanitize=" + sanitizer},
|
|
ldFlags: []string{"-fsanitize=" + sanitizer},
|
|
}
|
|
|
|
if testing.Verbose() {
|
|
c.goFlags = append(c.goFlags, "-x")
|
|
}
|
|
|
|
switch sanitizer {
|
|
case "memory":
|
|
c.goFlags = append(c.goFlags, "-msan")
|
|
|
|
case "thread":
|
|
c.goFlags = append(c.goFlags, "--installsuffix=tsan")
|
|
compiler, _ := compilerVersion()
|
|
if compiler.name == "gcc" {
|
|
c.cFlags = append(c.cFlags, "-fPIC")
|
|
c.ldFlags = append(c.ldFlags, "-fPIC", "-static-libtsan")
|
|
}
|
|
|
|
case "address":
|
|
c.goFlags = append(c.goFlags, "-asan")
|
|
// Set the debug mode to print the C stack trace.
|
|
c.cFlags = append(c.cFlags, "-g")
|
|
|
|
default:
|
|
panic(fmt.Sprintf("unrecognized sanitizer: %q", sanitizer))
|
|
}
|
|
|
|
if configs.m == nil {
|
|
configs.m = make(map[string]*config)
|
|
}
|
|
configs.m[sanitizer] = c
|
|
return c
|
|
}
|
|
|
|
// goCmd returns a Cmd that executes "go $subcommand $args" with appropriate
|
|
// additional flags and environment.
|
|
func (c *config) goCmd(subcommand string, args ...string) *exec.Cmd {
|
|
cmd := exec.Command("go", subcommand)
|
|
cmd.Args = append(cmd.Args, c.goFlags...)
|
|
cmd.Args = append(cmd.Args, args...)
|
|
replaceEnv(cmd, "CGO_CFLAGS", strings.Join(c.cFlags, " "))
|
|
replaceEnv(cmd, "CGO_LDFLAGS", strings.Join(c.ldFlags, " "))
|
|
return cmd
|
|
}
|
|
|
|
// skipIfCSanitizerBroken skips t if the C compiler does not produce working
|
|
// binaries as configured.
|
|
func (c *config) skipIfCSanitizerBroken(t *testing.T) {
|
|
check := &c.sanitizerCheck
|
|
check.once.Do(func() {
|
|
check.skip, check.err = c.checkCSanitizer()
|
|
})
|
|
if check.err != nil {
|
|
t.Helper()
|
|
if check.skip {
|
|
t.Skip(check.err)
|
|
}
|
|
t.Fatal(check.err)
|
|
}
|
|
}
|
|
|
|
var cMain = []byte(`
|
|
int main() {
|
|
return 0;
|
|
}
|
|
`)
|
|
|
|
func (c *config) checkCSanitizer() (skip bool, err error) {
|
|
dir, err := os.MkdirTemp("", c.sanitizer)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to create temp directory: %v", err)
|
|
}
|
|
defer os.RemoveAll(dir)
|
|
|
|
src := filepath.Join(dir, "return0.c")
|
|
if err := os.WriteFile(src, cMain, 0600); err != nil {
|
|
return false, fmt.Errorf("failed to write C source file: %v", err)
|
|
}
|
|
|
|
dst := filepath.Join(dir, "return0")
|
|
cmd, err := cc(c.cFlags...)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
cmd.Args = append(cmd.Args, c.ldFlags...)
|
|
cmd.Args = append(cmd.Args, "-o", dst, src)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
if bytes.Contains(out, []byte("-fsanitize")) &&
|
|
(bytes.Contains(out, []byte("unrecognized")) ||
|
|
bytes.Contains(out, []byte("unsupported"))) {
|
|
return true, errors.New(string(out))
|
|
}
|
|
return true, fmt.Errorf("%#q failed: %v\n%s", strings.Join(cmd.Args, " "), err, out)
|
|
}
|
|
|
|
if out, err := exec.Command(dst).CombinedOutput(); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return true, fmt.Errorf("%#q failed to produce executable: %v", strings.Join(cmd.Args, " "), err)
|
|
}
|
|
snippet, _, _ := bytes.Cut(out, []byte("\n"))
|
|
return true, fmt.Errorf("%#q generated broken executable: %v\n%s", strings.Join(cmd.Args, " "), err, snippet)
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// skipIfRuntimeIncompatible skips t if the Go runtime is suspected not to work
|
|
// with cgo as configured.
|
|
func (c *config) skipIfRuntimeIncompatible(t *testing.T) {
|
|
check := &c.runtimeCheck
|
|
check.once.Do(func() {
|
|
check.skip, check.err = c.checkRuntime()
|
|
})
|
|
if check.err != nil {
|
|
t.Helper()
|
|
if check.skip {
|
|
t.Skip(check.err)
|
|
}
|
|
t.Fatal(check.err)
|
|
}
|
|
}
|
|
|
|
func (c *config) checkRuntime() (skip bool, err error) {
|
|
if c.sanitizer != "thread" {
|
|
return false, nil
|
|
}
|
|
|
|
// libcgo.h sets CGO_TSAN if it detects TSAN support in the C compiler.
|
|
// Dump the preprocessor defines to check that works.
|
|
// (Sometimes it doesn't: see https://golang.org/issue/15983.)
|
|
cmd, err := cc(c.cFlags...)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
cmd.Args = append(cmd.Args, "-dM", "-E", "../../../src/runtime/cgo/libcgo.h")
|
|
cmdStr := strings.Join(cmd.Args, " ")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return false, fmt.Errorf("%#q exited with %v\n%s", cmdStr, err, out)
|
|
}
|
|
if !bytes.Contains(out, []byte("#define CGO_TSAN")) {
|
|
return true, fmt.Errorf("%#q did not define CGO_TSAN", cmdStr)
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// srcPath returns the path to the given file relative to this test's source tree.
|
|
func srcPath(path string) string {
|
|
return filepath.Join("testdata", path)
|
|
}
|
|
|
|
// A tempDir manages a temporary directory within a test.
|
|
type tempDir struct {
|
|
base string
|
|
}
|
|
|
|
func (d *tempDir) RemoveAll(t *testing.T) {
|
|
t.Helper()
|
|
if d.base == "" {
|
|
return
|
|
}
|
|
if err := os.RemoveAll(d.base); err != nil {
|
|
t.Fatalf("Failed to remove temp dir: %v", err)
|
|
}
|
|
}
|
|
|
|
func (d *tempDir) Join(name string) string {
|
|
return filepath.Join(d.base, name)
|
|
}
|
|
|
|
func newTempDir(t *testing.T) *tempDir {
|
|
t.Helper()
|
|
dir, err := os.MkdirTemp("", filepath.Dir(t.Name()))
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
return &tempDir{base: dir}
|
|
}
|
|
|
|
// hangProneCmd returns an exec.Cmd for a command that is likely to hang.
|
|
//
|
|
// If one of these tests hangs, the caller is likely to kill the test process
|
|
// using SIGINT, which will be sent to all of the processes in the test's group.
|
|
// Unfortunately, TSAN in particular is prone to dropping signals, so the SIGINT
|
|
// may terminate the test binary but leave the subprocess running. hangProneCmd
|
|
// configures subprocess to receive SIGKILL instead to ensure that it won't
|
|
// leak.
|
|
func hangProneCmd(name string, arg ...string) *exec.Cmd {
|
|
cmd := exec.Command(name, arg...)
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
Pdeathsig: syscall.SIGKILL,
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
// mSanSupported is a copy of the function cmd/internal/sys.MSanSupported,
|
|
// because the internal pacakage can't be used here.
|
|
func mSanSupported(goos, goarch string) bool {
|
|
switch goos {
|
|
case "linux":
|
|
return goarch == "amd64" || goarch == "arm64"
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// aSanSupported is a copy of the function cmd/internal/sys.ASanSupported,
|
|
// because the internal pacakage can't be used here.
|
|
func aSanSupported(goos, goarch string) bool {
|
|
switch goos {
|
|
case "linux":
|
|
return goarch == "amd64" || goarch == "arm64" || goarch == "riscv64"
|
|
default:
|
|
return false
|
|
}
|
|
}
|