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