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
= ioutil
.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
= os
.Environ()
88 cmd
.Env
= append(cmd
.Env
, key
+"="+value
)
91 // mustRun executes t and fails cmd with a well-formatted message if it fails.
92 func mustRun(t
*testing
.T
, cmd
*exec
.Cmd
) {
94 out
, err
:= cmd
.CombinedOutput()
96 t
.Fatalf("%#q exited with %v\n%s", strings
.Join(cmd
.Args
, " "), err
, out
)
100 // cc returns a cmd that executes `$(go env CC) $(go env GOGCCFLAGS) $args`.
101 func cc(args
...string) (*exec
.Cmd
, error
) {
102 CC
, err
:= goEnv("CC")
107 GOGCCFLAGS
, err
:= goEnv("GOGCCFLAGS")
112 // Split GOGCCFLAGS, respecting quoting.
114 // TODO(bcmills): This code also appears in
115 // misc/cgo/testcarchive/carchive_test.go, and perhaps ought to go in
116 // src/cmd/dist/test.go as well. Figure out where to put it so that it can be
123 for i
, c
:= range GOGCCFLAGS
{
124 if quote
== '\000' && unicode
.IsSpace(c
) {
126 flags
= append(flags
, GOGCCFLAGS
[start
:i
])
134 if quote
== '\000' && !backslash
&& (c
== '"' || c
== '\'') {
137 } else if !backslash
&& quote
== c
{
139 } else if (quote
== '\000' || quote
== '"') && !backslash
&& c
== '\\' {
147 flags
= append(flags
, GOGCCFLAGS
[start
:])
150 cmd
:= exec
.Command(CC
, flags
...)
151 cmd
.Args
= append(cmd
.Args
, args
...)
155 type version
struct {
160 var compiler
struct {
166 // compilerVersion detects the version of $(go env CC).
168 // It returns a non-nil error if the compiler matches a known version schema but
169 // the version could not be parsed, or if $(go env CC) could not be determined.
170 func compilerVersion() (version
, error
) {
171 compiler
.Once
.Do(func() {
172 compiler
.err
= func() error
{
173 compiler
.name
= "unknown"
175 cmd
, err
:= cc("--version")
179 out
, err
:= cmd
.Output()
181 // Compiler does not support "--version" flag: not Clang or GCC.
186 if bytes
.HasPrefix(out
, []byte("gcc")) {
187 compiler
.name
= "gcc"
189 cmd
, err
:= cc("-dumpversion")
193 out
, err
:= cmd
.Output()
195 // gcc, but does not support gcc's "-dumpversion" flag?!
198 gccRE
:= regexp
.MustCompile(`(\d+)\.(\d+)`)
199 match
= gccRE
.FindSubmatch(out
)
201 clangRE
:= regexp
.MustCompile(`clang version (\d+)\.(\d+)`)
202 if match
= clangRE
.FindSubmatch(out
); len(match
) > 0 {
203 compiler
.name
= "clang"
208 return nil // "unknown"
210 if compiler
.major
, err
= strconv
.Atoi(string(match
[1])); err
!= nil {
213 if compiler
.minor
, err
= strconv
.Atoi(string(match
[2])); err
!= nil {
219 return compiler
.version
, compiler
.err
222 type compilerCheck
struct {
225 skip
bool // If true, skip with err instead of failing with it.
231 cFlags
, ldFlags
, goFlags
[]string
233 sanitizerCheck
, runtimeCheck compilerCheck
241 // configure returns the configuration for the given sanitizer.
242 func configure(sanitizer
string) *config
{
244 defer configs
.Unlock()
245 if c
, ok
:= configs
.m
[sanitizer
]; ok
{
250 sanitizer
: sanitizer
,
251 cFlags
: []string{"-fsanitize=" + sanitizer
},
252 ldFlags
: []string{"-fsanitize=" + sanitizer
},
255 if testing
.Verbose() {
256 c
.goFlags
= append(c
.goFlags
, "-x")
261 c
.goFlags
= append(c
.goFlags
, "-msan")
264 c
.goFlags
= append(c
.goFlags
, "--installsuffix=tsan")
265 compiler
, _
:= compilerVersion()
266 if compiler
.name
== "gcc" {
267 c
.cFlags
= append(c
.cFlags
, "-fPIC")
268 c
.ldFlags
= append(c
.ldFlags
, "-fPIC", "-static-libtsan")
272 panic(fmt
.Sprintf("unrecognized sanitizer: %q", sanitizer
))
275 if configs
.m
== nil {
276 configs
.m
= make(map[string]*config
)
278 configs
.m
[sanitizer
] = c
282 // goCmd returns a Cmd that executes "go $subcommand $args" with appropriate
283 // additional flags and environment.
284 func (c
*config
) goCmd(subcommand
string, args
...string) *exec
.Cmd
{
285 cmd
:= exec
.Command("go", subcommand
)
286 cmd
.Args
= append(cmd
.Args
, c
.goFlags
...)
287 cmd
.Args
= append(cmd
.Args
, args
...)
288 replaceEnv(cmd
, "CGO_CFLAGS", strings
.Join(c
.cFlags
, " "))
289 replaceEnv(cmd
, "CGO_LDFLAGS", strings
.Join(c
.ldFlags
, " "))
293 // skipIfCSanitizerBroken skips t if the C compiler does not produce working
294 // binaries as configured.
295 func (c
*config
) skipIfCSanitizerBroken(t
*testing
.T
) {
296 check
:= &c
.sanitizerCheck
297 check
.once
.Do(func() {
298 check
.skip
, check
.err
= c
.checkCSanitizer()
300 if check
.err
!= nil {
315 func (c
*config
) checkCSanitizer() (skip
bool, err error
) {
316 dir
, err
:= ioutil
.TempDir("", c
.sanitizer
)
318 return false, fmt
.Errorf("failed to create temp directory: %v", err
)
320 defer os
.RemoveAll(dir
)
322 src
:= filepath
.Join(dir
, "return0.c")
323 if err
:= ioutil
.WriteFile(src
, cMain
, 0600); err
!= nil {
324 return false, fmt
.Errorf("failed to write C source file: %v", err
)
327 dst
:= filepath
.Join(dir
, "return0")
328 cmd
, err
:= cc(c
.cFlags
...)
332 cmd
.Args
= append(cmd
.Args
, c
.ldFlags
...)
333 cmd
.Args
= append(cmd
.Args
, "-o", dst
, src
)
334 out
, err
:= cmd
.CombinedOutput()
336 if bytes
.Contains(out
, []byte("-fsanitize")) &&
337 (bytes
.Contains(out
, []byte("unrecognized")) ||
338 bytes
.Contains(out
, []byte("unsupported"))) {
339 return true, errors
.New(string(out
))
341 return true, fmt
.Errorf("%#q failed: %v\n%s", strings
.Join(cmd
.Args
, " "), err
, out
)
344 if out
, err
:= exec
.Command(dst
).CombinedOutput(); err
!= nil {
345 if os
.IsNotExist(err
) {
346 return true, fmt
.Errorf("%#q failed to produce executable: %v", strings
.Join(cmd
.Args
, " "), err
)
348 snippet
:= bytes
.SplitN(out
, []byte{'\n'}, 2)[0]
349 return true, fmt
.Errorf("%#q generated broken executable: %v\n%s", strings
.Join(cmd
.Args
, " "), err
, snippet
)
355 // skipIfRuntimeIncompatible skips t if the Go runtime is suspected not to work
356 // with cgo as configured.
357 func (c
*config
) skipIfRuntimeIncompatible(t
*testing
.T
) {
358 check
:= &c
.runtimeCheck
359 check
.once
.Do(func() {
360 check
.skip
, check
.err
= c
.checkRuntime()
362 if check
.err
!= nil {
371 func (c
*config
) checkRuntime() (skip
bool, err error
) {
372 if c
.sanitizer
!= "thread" {
376 // libcgo.h sets CGO_TSAN if it detects TSAN support in the C compiler.
377 // Dump the preprocessor defines to check that that works.
378 // (Sometimes it doesn't: see https://golang.org/issue/15983.)
379 cmd
, err
:= cc(c
.cFlags
...)
383 cmd
.Args
= append(cmd
.Args
, "-dM", "-E", "../../../src/runtime/cgo/libcgo.h")
384 out
, err
:= cmd
.CombinedOutput()
386 return false, fmt
.Errorf("%#q exited with %v\n%s", strings
.Join(cmd
.Args
, " "), err
, out
)
388 if !bytes
.Contains(out
, []byte("#define CGO_TSAN")) {
389 return true, fmt
.Errorf("%#q did not define CGO_TSAN")
394 // srcPath returns the path to the given file relative to this test's source tree.
395 func srcPath(path
string) string {
396 return filepath
.Join("src", path
)
399 // A tempDir manages a temporary directory within a test.
400 type tempDir
struct {
404 func (d
*tempDir
) RemoveAll(t
*testing
.T
) {
409 if err
:= os
.RemoveAll(d
.base
); err
!= nil {
410 t
.Fatalf("Failed to remove temp dir: %v", err
)
414 func (d
*tempDir
) Join(name
string) string {
415 return filepath
.Join(d
.base
, name
)
418 func newTempDir(t
*testing
.T
) *tempDir
{
420 dir
, err
:= ioutil
.TempDir("", filepath
.Dir(t
.Name()))
422 t
.Fatalf("Failed to create temp dir: %v", err
)
424 return &tempDir
{base
: dir
}
427 // hangProneCmd returns an exec.Cmd for a command that is likely to hang.
429 // If one of these tests hangs, the caller is likely to kill the test process
430 // using SIGINT, which will be sent to all of the processes in the test's group.
431 // Unfortunately, TSAN in particular is prone to dropping signals, so the SIGINT
432 // may terminate the test binary but leave the subprocess running. hangProneCmd
433 // configures subprocess to receive SIGKILL instead to ensure that it won't
435 func hangProneCmd(name
string, arg
...string) *exec
.Cmd
{
436 cmd
:= exec
.Command(name
, arg
...)
437 cmd
.SysProcAttr
= &syscall
.SysProcAttr
{
438 Pdeathsig
: syscall
.SIGKILL
,