123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 |
- // 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("-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
- }
- // 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
- }
- }
- 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
- }
- }
|