| // 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 |
| } |
| |
| 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 := 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 |
| } |
| } |