// 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"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"syscall"
"testing"
"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 = ioutil.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, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("%#q exited with %v\n%s", strings.Join(cmd.Args, " "), 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("-dumpversion")
if err != nil {
return err
}
out, err := cmd.Output()
if err != nil {
// gcc, but does not support gcc's "-dumpversion" flag?!
return err
}
gccRE := regexp.MustCompile(`(\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
}
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")
}
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 := ioutil.TempDir("", 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 := ioutil.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.SplitN(out, []byte{'\n'}, 2)[0]
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("src", 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 := ioutil.TempDir("", 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
}