1 // Copyright 2017 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
5 // sanitizers_test checks the use of Go with sanitizers like msan, asan, etc.
6 // See https://github.com/google/sanitizers.
7 package sanitizers_test
27 var overcommit struct {
33 // requireOvercommit skips t if the kernel does not allow overcommit.
34 func requireOvercommit(t *testing.T) {
37 overcommit.Once.Do(func() {
39 out, overcommit.err = os.ReadFile("/proc/sys/vm/overcommit_memory")
40 if overcommit.err != nil {
43 overcommit.value, overcommit.err = strconv.Atoi(string(bytes.TrimSpace(out)))
46 if overcommit.err != nil {
47 t.Skipf("couldn't determine vm.overcommit_memory (%v); assuming no overcommit", overcommit.err)
49 if overcommit.value == 2 {
50 t.Skip("vm.overcommit_memory=2")
60 // goEnv returns the output of $(go env) as a map.
61 func goEnv(key string) (string, error) {
64 out, env.err = exec.Command("go", "env", "-json").Output()
69 env.m = make(map[string]string)
70 env.err = json.Unmarshal(out, &env.m)
78 return "", fmt.Errorf("`go env`: no entry for %v", key)
83 // replaceEnv sets the key environment variable to value in cmd.
84 func replaceEnv(cmd *exec.Cmd, key, value string) {
86 cmd.Env = cmd.Environ()
88 cmd.Env = append(cmd.Env, key+"="+value)
91 // appendExperimentEnv appends comma-separated experiments to GOEXPERIMENT.
92 func appendExperimentEnv(cmd *exec.Cmd, experiments []string) {
94 cmd.Env = cmd.Environ()
96 exps := strings.Join(experiments, ",")
97 for _, evar := range cmd.Env {
98 c := strings.SplitN(evar, "=", 2)
99 if c[0] == "GOEXPERIMENT" {
100 exps = c[1] + "," + exps
103 cmd.Env = append(cmd.Env, "GOEXPERIMENT="+exps)
106 // mustRun executes t and fails cmd with a well-formatted message if it fails.
107 func mustRun(t *testing.T, cmd *exec.Cmd) {
109 out := new(strings.Builder)
115 t.Fatalf("%v: %v", cmd, err)
118 if deadline, ok := t.Deadline(); ok {
119 timeout := time.Until(deadline)
120 timeout -= timeout / 10 // Leave 10% headroom for logging and cleanup.
121 timer := time.AfterFunc(timeout, func() {
122 cmd.Process.Signal(syscall.SIGQUIT)
127 if err := cmd.Wait(); err != nil {
128 t.Fatalf("%v exited with %v\n%s", cmd, err, out)
132 // cc returns a cmd that executes `$(go env CC) $(go env GOGCCFLAGS) $args`.
133 func cc(args ...string) (*exec.Cmd, error) {
134 CC, err := goEnv("CC")
139 GOGCCFLAGS, err := goEnv("GOGCCFLAGS")
144 // Split GOGCCFLAGS, respecting quoting.
146 // TODO(bcmills): This code also appears in
147 // misc/cgo/testcarchive/carchive_test.go, and perhaps ought to go in
148 // src/cmd/dist/test.go as well. Figure out where to put it so that it can be
155 for i, c := range GOGCCFLAGS {
156 if quote == '\000' && unicode.IsSpace(c) {
158 flags = append(flags, GOGCCFLAGS[start:i])
166 if quote == '\000' && !backslash && (c == '"' || c == '\'') {
169 } else if !backslash && quote == c {
171 } else if (quote == '\000' || quote == '"') && !backslash && c == '\\' {
179 flags = append(flags, GOGCCFLAGS[start:])
182 cmd := exec.Command(CC, flags...)
183 cmd.Args = append(cmd.Args, args...)
187 type version struct {
192 var compiler struct {
198 // compilerVersion detects the version of $(go env CC).
200 // It returns a non-nil error if the compiler matches a known version schema but
201 // the version could not be parsed, or if $(go env CC) could not be determined.
202 func compilerVersion() (version, error) {
203 compiler.Once.Do(func() {
204 compiler.err = func() error {
205 compiler.name = "unknown"
207 cmd, err := cc("--version")
211 out, err := cmd.Output()
213 // Compiler does not support "--version" flag: not Clang or GCC.
218 if bytes.HasPrefix(out, []byte("gcc")) {
219 compiler.name = "gcc"
220 cmd, err := cc("-dumpfullversion", "-dumpversion")
224 out, err := cmd.Output()
226 // gcc, but does not support gcc's "-v" flag?!
229 gccRE := regexp.MustCompile(`(\d+)\.(\d+)`)
230 match = gccRE.FindSubmatch(out)
232 clangRE := regexp.MustCompile(`clang version (\d+)\.(\d+)`)
233 if match = clangRE.FindSubmatch(out); len(match) > 0 {
234 compiler.name = "clang"
239 return nil // "unknown"
241 if compiler.major, err = strconv.Atoi(string(match[1])); err != nil {
244 if compiler.minor, err = strconv.Atoi(string(match[2])); err != nil {
250 return compiler.version, compiler.err
253 // compilerSupportsLocation reports whether the compiler should be
254 // able to provide file/line information in backtraces.
255 func compilerSupportsLocation() bool {
256 compiler, err := compilerVersion()
260 switch compiler.name {
262 return compiler.major >= 10
270 // compilerRequiredTsanVersion reports whether the compiler is the version required by Tsan.
271 // Only restrictions for ppc64le are known; otherwise return true.
272 func compilerRequiredTsanVersion(goos, goarch string) bool {
273 compiler, err := compilerVersion()
277 if compiler.name == "gcc" && goarch == "ppc64le" {
278 return compiler.major >= 9
283 // compilerRequiredAsanVersion reports whether the compiler is the version required by Asan.
284 func compilerRequiredAsanVersion(goos, goarch string) bool {
285 compiler, err := compilerVersion()
289 switch compiler.name {
291 if goarch == "ppc64le" {
292 return compiler.major >= 9
294 return compiler.major >= 7
296 return compiler.major >= 9
302 type compilerCheck struct {
305 skip bool // If true, skip with err instead of failing with it.
311 cFlags, ldFlags, goFlags []string
313 sanitizerCheck, runtimeCheck compilerCheck
321 // configure returns the configuration for the given sanitizer.
322 func configure(sanitizer string) *config {
324 defer configs.Unlock()
325 if c, ok := configs.m[sanitizer]; ok {
330 sanitizer: sanitizer,
331 cFlags: []string{"-fsanitize=" + sanitizer},
332 ldFlags: []string{"-fsanitize=" + sanitizer},
335 if testing.Verbose() {
336 c.goFlags = append(c.goFlags, "-x")
341 c.goFlags = append(c.goFlags, "-msan")
344 c.goFlags = append(c.goFlags, "--installsuffix=tsan")
345 compiler, _ := compilerVersion()
346 if compiler.name == "gcc" {
347 c.cFlags = append(c.cFlags, "-fPIC")
348 c.ldFlags = append(c.ldFlags, "-fPIC", "-static-libtsan")
352 c.goFlags = append(c.goFlags, "-asan")
353 // Set the debug mode to print the C stack trace.
354 c.cFlags = append(c.cFlags, "-g")
357 c.goFlags = append(c.goFlags, "-tags=libfuzzer", "-gcflags=-d=libfuzzer")
360 panic(fmt.Sprintf("unrecognized sanitizer: %q", sanitizer))
363 if configs.m == nil {
364 configs.m = make(map[string]*config)
366 configs.m[sanitizer] = c
370 // goCmd returns a Cmd that executes "go $subcommand $args" with appropriate
371 // additional flags and environment.
372 func (c *config) goCmd(subcommand string, args ...string) *exec.Cmd {
373 return c.goCmdWithExperiments(subcommand, args, nil)
376 // goCmdWithExperiments returns a Cmd that executes
377 // "GOEXPERIMENT=$experiments go $subcommand $args" with appropriate
378 // additional flags and CGO-related environment variables.
379 func (c *config) goCmdWithExperiments(subcommand string, args []string, experiments []string) *exec.Cmd {
380 cmd := exec.Command("go", subcommand)
381 cmd.Args = append(cmd.Args, c.goFlags...)
382 cmd.Args = append(cmd.Args, args...)
383 replaceEnv(cmd, "CGO_CFLAGS", strings.Join(c.cFlags, " "))
384 replaceEnv(cmd, "CGO_LDFLAGS", strings.Join(c.ldFlags, " "))
385 appendExperimentEnv(cmd, experiments)
389 // skipIfCSanitizerBroken skips t if the C compiler does not produce working
390 // binaries as configured.
391 func (c *config) skipIfCSanitizerBroken(t *testing.T) {
392 check := &c.sanitizerCheck
393 check.once.Do(func() {
394 check.skip, check.err = c.checkCSanitizer()
396 if check.err != nil {
411 var cLibFuzzerInput = []byte(`
413 int LLVMFuzzerTestOneInput(char *data, size_t size) {
418 func (c *config) checkCSanitizer() (skip bool, err error) {
419 dir, err := os.MkdirTemp("", c.sanitizer)
421 return false, fmt.Errorf("failed to create temp directory: %v", err)
423 defer os.RemoveAll(dir)
425 src := filepath.Join(dir, "return0.c")
427 if c.sanitizer == "fuzzer" {
428 // libFuzzer generates the main function itself, and uses a different input.
429 cInput = cLibFuzzerInput
431 if err := os.WriteFile(src, cInput, 0600); err != nil {
432 return false, fmt.Errorf("failed to write C source file: %v", err)
435 dst := filepath.Join(dir, "return0")
436 cmd, err := cc(c.cFlags...)
440 cmd.Args = append(cmd.Args, c.ldFlags...)
441 cmd.Args = append(cmd.Args, "-o", dst, src)
442 out, err := cmd.CombinedOutput()
444 if bytes.Contains(out, []byte("-fsanitize")) &&
445 (bytes.Contains(out, []byte("unrecognized")) ||
446 bytes.Contains(out, []byte("unsupported"))) {
447 return true, errors.New(string(out))
449 return true, fmt.Errorf("%#q failed: %v\n%s", strings.Join(cmd.Args, " "), err, out)
452 if c.sanitizer == "fuzzer" {
453 // For fuzzer, don't try running the test binary. It never finishes.
457 if out, err := exec.Command(dst).CombinedOutput(); err != nil {
458 if os.IsNotExist(err) {
459 return true, fmt.Errorf("%#q failed to produce executable: %v", strings.Join(cmd.Args, " "), err)
461 snippet, _, _ := bytes.Cut(out, []byte("\n"))
462 return true, fmt.Errorf("%#q generated broken executable: %v\n%s", strings.Join(cmd.Args, " "), err, snippet)
468 // skipIfRuntimeIncompatible skips t if the Go runtime is suspected not to work
469 // with cgo as configured.
470 func (c *config) skipIfRuntimeIncompatible(t *testing.T) {
471 check := &c.runtimeCheck
472 check.once.Do(func() {
473 check.skip, check.err = c.checkRuntime()
475 if check.err != nil {
484 func (c *config) checkRuntime() (skip bool, err error) {
485 if c.sanitizer != "thread" {
489 // libcgo.h sets CGO_TSAN if it detects TSAN support in the C compiler.
490 // Dump the preprocessor defines to check that works.
491 // (Sometimes it doesn't: see https://golang.org/issue/15983.)
492 cmd, err := cc(c.cFlags...)
496 cmd.Args = append(cmd.Args, "-dM", "-E", "../../../src/runtime/cgo/libcgo.h")
497 cmdStr := strings.Join(cmd.Args, " ")
498 out, err := cmd.CombinedOutput()
500 return false, fmt.Errorf("%#q exited with %v\n%s", cmdStr, err, out)
502 if !bytes.Contains(out, []byte("#define CGO_TSAN")) {
503 return true, fmt.Errorf("%#q did not define CGO_TSAN", cmdStr)
508 // srcPath returns the path to the given file relative to this test's source tree.
509 func srcPath(path string) string {
510 return filepath.Join("testdata", path)
513 // A tempDir manages a temporary directory within a test.
514 type tempDir struct {
518 func (d *tempDir) RemoveAll(t *testing.T) {
523 if err := os.RemoveAll(d.base); err != nil {
524 t.Fatalf("Failed to remove temp dir: %v", err)
528 func (d *tempDir) Base() string {
532 func (d *tempDir) Join(name string) string {
533 return filepath.Join(d.base, name)
536 func newTempDir(t *testing.T) *tempDir {
538 dir, err := os.MkdirTemp("", filepath.Dir(t.Name()))
540 t.Fatalf("Failed to create temp dir: %v", err)
542 return &tempDir{base: dir}
545 // hangProneCmd returns an exec.Cmd for a command that is likely to hang.
547 // If one of these tests hangs, the caller is likely to kill the test process
548 // using SIGINT, which will be sent to all of the processes in the test's group.
549 // Unfortunately, TSAN in particular is prone to dropping signals, so the SIGINT
550 // may terminate the test binary but leave the subprocess running. hangProneCmd
551 // configures subprocess to receive SIGKILL instead to ensure that it won't
553 func hangProneCmd(name string, arg ...string) *exec.Cmd {
554 cmd := exec.Command(name, arg...)
555 cmd.SysProcAttr = &syscall.SysProcAttr{
556 Pdeathsig: syscall.SIGKILL,
561 // mSanSupported is a copy of the function cmd/internal/sys.MSanSupported,
562 // because the internal package can't be used here.
563 func mSanSupported(goos, goarch string) bool {
566 return goarch == "amd64" || goarch == "arm64"
568 return goarch == "amd64"
574 // aSanSupported is a copy of the function cmd/internal/sys.ASanSupported,
575 // because the internal package can't be used here.
576 func aSanSupported(goos, goarch string) bool {
579 return goarch == "amd64" || goarch == "arm64" || goarch == "riscv64" || goarch == "ppc64le"