|  | // 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" | 
|  | "io/ioutil" | 
|  | "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 = ioutil.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 := ioutil.TempDir("", 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 := ioutil.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.SplitN(out, []byte{'\n'}, 2)[0] | 
|  | 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("src", 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 := ioutil.TempDir("", 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 | 
|  | } |