mirror of
https://github.com/golang/go
synced 2024-11-08 13:56:21 -07:00
f52b4ec63d
With this patch, -asan option can detect the error memory access to global variables. So this patch makes a few changes: 1. Add the asanregisterglobals runtime support function, which calls asan runtime function _asan_register_globals to register global variables. 2. Create a new initialization function for the package being compiled. This function initializes an array of instrumented global variables and pass it to function runtime.asanregisterglobals. An instrumented global variable has trailing redzone. 3. Writes the new size of instrumented global variables that have trailing redzones into object file. 4. Notice that the current implementation is only compatible with the ASan library from version v7 to v9. Therefore, using the -asan option requires that the gcc version is not less than 7 and the clang version is less than 4, otherwise a segmentation fault will occur. So this patch adds a check on whether the compiler being used is a supported version in cmd/go. Updates #44853. Change-Id: Ib877a817209ab2be68a8e22c418fe4a4a20880fc Reviewed-on: https://go-review.googlesource.com/c/go/+/401775 Reviewed-by: Ian Lance Taylor <iant@google.com> Reviewed-by: Bryan Mills <bcmills@google.com> Run-TryBot: Bryan Mills <bcmills@google.com> Auto-Submit: Bryan Mills <bcmills@google.com> TryBot-Result: Gopher Robot <gobot@golang.org>
501 lines
12 KiB
Go
501 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"
|
|
"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, 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("-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"
|
|
default:
|
|
return false
|
|
}
|
|
}
|