131
|
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.
|
|
4
|
|
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
|
|
8
|
|
9 import (
|
|
10 "bytes"
|
|
11 "encoding/json"
|
|
12 "errors"
|
|
13 "fmt"
|
|
14 "io/ioutil"
|
|
15 "os"
|
|
16 "os/exec"
|
|
17 "path/filepath"
|
|
18 "regexp"
|
|
19 "strconv"
|
|
20 "strings"
|
|
21 "sync"
|
|
22 "syscall"
|
|
23 "testing"
|
|
24 "unicode"
|
|
25 )
|
|
26
|
|
27 var overcommit struct {
|
|
28 sync.Once
|
|
29 value int
|
|
30 err error
|
|
31 }
|
|
32
|
|
33 // requireOvercommit skips t if the kernel does not allow overcommit.
|
|
34 func requireOvercommit(t *testing.T) {
|
|
35 t.Helper()
|
|
36
|
|
37 overcommit.Once.Do(func() {
|
|
38 var out []byte
|
|
39 out, overcommit.err = ioutil.ReadFile("/proc/sys/vm/overcommit_memory")
|
|
40 if overcommit.err != nil {
|
|
41 return
|
|
42 }
|
|
43 overcommit.value, overcommit.err = strconv.Atoi(string(bytes.TrimSpace(out)))
|
|
44 })
|
|
45
|
|
46 if overcommit.err != nil {
|
|
47 t.Skipf("couldn't determine vm.overcommit_memory (%v); assuming no overcommit", overcommit.err)
|
|
48 }
|
|
49 if overcommit.value == 2 {
|
|
50 t.Skip("vm.overcommit_memory=2")
|
|
51 }
|
|
52 }
|
|
53
|
|
54 var env struct {
|
|
55 sync.Once
|
|
56 m map[string]string
|
|
57 err error
|
|
58 }
|
|
59
|
|
60 // goEnv returns the output of $(go env) as a map.
|
|
61 func goEnv(key string) (string, error) {
|
|
62 env.Once.Do(func() {
|
|
63 var out []byte
|
|
64 out, env.err = exec.Command("go", "env", "-json").Output()
|
|
65 if env.err != nil {
|
|
66 return
|
|
67 }
|
|
68
|
|
69 env.m = make(map[string]string)
|
|
70 env.err = json.Unmarshal(out, &env.m)
|
|
71 })
|
|
72 if env.err != nil {
|
|
73 return "", env.err
|
|
74 }
|
|
75
|
|
76 v, ok := env.m[key]
|
|
77 if !ok {
|
|
78 return "", fmt.Errorf("`go env`: no entry for %v", key)
|
|
79 }
|
|
80 return v, nil
|
|
81 }
|
|
82
|
|
83 // replaceEnv sets the key environment variable to value in cmd.
|
|
84 func replaceEnv(cmd *exec.Cmd, key, value string) {
|
|
85 if cmd.Env == nil {
|
|
86 cmd.Env = os.Environ()
|
|
87 }
|
|
88 cmd.Env = append(cmd.Env, key+"="+value)
|
|
89 }
|
|
90
|
|
91 // mustRun executes t and fails cmd with a well-formatted message if it fails.
|
|
92 func mustRun(t *testing.T, cmd *exec.Cmd) {
|
|
93 t.Helper()
|
|
94 out, err := cmd.CombinedOutput()
|
|
95 if err != nil {
|
|
96 t.Fatalf("%#q exited with %v\n%s", strings.Join(cmd.Args, " "), err, out)
|
|
97 }
|
|
98 }
|
|
99
|
|
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")
|
|
103 if err != nil {
|
|
104 return nil, err
|
|
105 }
|
|
106
|
|
107 GOGCCFLAGS, err := goEnv("GOGCCFLAGS")
|
|
108 if err != nil {
|
|
109 return nil, err
|
|
110 }
|
|
111
|
|
112 // Split GOGCCFLAGS, respecting quoting.
|
|
113 //
|
|
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
|
|
117 // shared.
|
|
118 var flags []string
|
|
119 quote := '\000'
|
|
120 start := 0
|
|
121 lastSpace := true
|
|
122 backslash := false
|
|
123 for i, c := range GOGCCFLAGS {
|
|
124 if quote == '\000' && unicode.IsSpace(c) {
|
|
125 if !lastSpace {
|
|
126 flags = append(flags, GOGCCFLAGS[start:i])
|
|
127 lastSpace = true
|
|
128 }
|
|
129 } else {
|
|
130 if lastSpace {
|
|
131 start = i
|
|
132 lastSpace = false
|
|
133 }
|
|
134 if quote == '\000' && !backslash && (c == '"' || c == '\'') {
|
|
135 quote = c
|
|
136 backslash = false
|
|
137 } else if !backslash && quote == c {
|
|
138 quote = '\000'
|
|
139 } else if (quote == '\000' || quote == '"') && !backslash && c == '\\' {
|
|
140 backslash = true
|
|
141 } else {
|
|
142 backslash = false
|
|
143 }
|
|
144 }
|
|
145 }
|
|
146 if !lastSpace {
|
|
147 flags = append(flags, GOGCCFLAGS[start:])
|
|
148 }
|
|
149
|
|
150 cmd := exec.Command(CC, flags...)
|
|
151 cmd.Args = append(cmd.Args, args...)
|
|
152 return cmd, nil
|
|
153 }
|
|
154
|
|
155 type version struct {
|
|
156 name string
|
|
157 major, minor int
|
|
158 }
|
|
159
|
|
160 var compiler struct {
|
|
161 sync.Once
|
|
162 version
|
|
163 err error
|
|
164 }
|
|
165
|
|
166 // compilerVersion detects the version of $(go env CC).
|
|
167 //
|
|
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"
|
|
174
|
|
175 cmd, err := cc("--version")
|
|
176 if err != nil {
|
|
177 return err
|
|
178 }
|
|
179 out, err := cmd.Output()
|
|
180 if err != nil {
|
|
181 // Compiler does not support "--version" flag: not Clang or GCC.
|
|
182 return nil
|
|
183 }
|
|
184
|
|
185 var match [][]byte
|
|
186 if bytes.HasPrefix(out, []byte("gcc")) {
|
|
187 compiler.name = "gcc"
|
|
188
|
|
189 cmd, err := cc("-dumpversion")
|
|
190 if err != nil {
|
|
191 return err
|
|
192 }
|
|
193 out, err := cmd.Output()
|
|
194 if err != nil {
|
|
195 // gcc, but does not support gcc's "-dumpversion" flag?!
|
|
196 return err
|
|
197 }
|
|
198 gccRE := regexp.MustCompile(`(\d+)\.(\d+)`)
|
|
199 match = gccRE.FindSubmatch(out)
|
|
200 } else {
|
|
201 clangRE := regexp.MustCompile(`clang version (\d+)\.(\d+)`)
|
|
202 if match = clangRE.FindSubmatch(out); len(match) > 0 {
|
|
203 compiler.name = "clang"
|
|
204 }
|
|
205 }
|
|
206
|
|
207 if len(match) < 3 {
|
|
208 return nil // "unknown"
|
|
209 }
|
|
210 if compiler.major, err = strconv.Atoi(string(match[1])); err != nil {
|
|
211 return err
|
|
212 }
|
|
213 if compiler.minor, err = strconv.Atoi(string(match[2])); err != nil {
|
|
214 return err
|
|
215 }
|
|
216 return nil
|
|
217 }()
|
|
218 })
|
|
219 return compiler.version, compiler.err
|
|
220 }
|
|
221
|
|
222 type compilerCheck struct {
|
|
223 once sync.Once
|
|
224 err error
|
|
225 skip bool // If true, skip with err instead of failing with it.
|
|
226 }
|
|
227
|
|
228 type config struct {
|
|
229 sanitizer string
|
|
230
|
|
231 cFlags, ldFlags, goFlags []string
|
|
232
|
|
233 sanitizerCheck, runtimeCheck compilerCheck
|
|
234 }
|
|
235
|
|
236 var configs struct {
|
|
237 sync.Mutex
|
|
238 m map[string]*config
|
|
239 }
|
|
240
|
|
241 // configure returns the configuration for the given sanitizer.
|
|
242 func configure(sanitizer string) *config {
|
|
243 configs.Lock()
|
|
244 defer configs.Unlock()
|
|
245 if c, ok := configs.m[sanitizer]; ok {
|
|
246 return c
|
|
247 }
|
|
248
|
|
249 c := &config{
|
|
250 sanitizer: sanitizer,
|
|
251 cFlags: []string{"-fsanitize=" + sanitizer},
|
|
252 ldFlags: []string{"-fsanitize=" + sanitizer},
|
|
253 }
|
|
254
|
|
255 if testing.Verbose() {
|
|
256 c.goFlags = append(c.goFlags, "-x")
|
|
257 }
|
|
258
|
|
259 switch sanitizer {
|
|
260 case "memory":
|
|
261 c.goFlags = append(c.goFlags, "-msan")
|
|
262
|
|
263 case "thread":
|
|
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")
|
|
269 }
|
|
270
|
|
271 default:
|
|
272 panic(fmt.Sprintf("unrecognized sanitizer: %q", sanitizer))
|
|
273 }
|
|
274
|
|
275 if configs.m == nil {
|
|
276 configs.m = make(map[string]*config)
|
|
277 }
|
|
278 configs.m[sanitizer] = c
|
|
279 return c
|
|
280 }
|
|
281
|
|
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, " "))
|
|
290 return cmd
|
|
291 }
|
|
292
|
|
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()
|
|
299 })
|
|
300 if check.err != nil {
|
|
301 t.Helper()
|
|
302 if check.skip {
|
|
303 t.Skip(check.err)
|
|
304 }
|
|
305 t.Fatal(check.err)
|
|
306 }
|
|
307 }
|
|
308
|
|
309 var cMain = []byte(`
|
|
310 int main() {
|
|
311 return 0;
|
|
312 }
|
|
313 `)
|
|
314
|
|
315 func (c *config) checkCSanitizer() (skip bool, err error) {
|
|
316 dir, err := ioutil.TempDir("", c.sanitizer)
|
|
317 if err != nil {
|
|
318 return false, fmt.Errorf("failed to create temp directory: %v", err)
|
|
319 }
|
|
320 defer os.RemoveAll(dir)
|
|
321
|
|
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)
|
|
325 }
|
|
326
|
|
327 dst := filepath.Join(dir, "return0")
|
|
328 cmd, err := cc(c.cFlags...)
|
|
329 if err != nil {
|
|
330 return false, err
|
|
331 }
|
|
332 cmd.Args = append(cmd.Args, c.ldFlags...)
|
|
333 cmd.Args = append(cmd.Args, "-o", dst, src)
|
|
334 out, err := cmd.CombinedOutput()
|
|
335 if err != nil {
|
|
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))
|
|
340 }
|
|
341 return true, fmt.Errorf("%#q failed: %v\n%s", strings.Join(cmd.Args, " "), err, out)
|
|
342 }
|
|
343
|
|
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)
|
|
347 }
|
|
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)
|
|
350 }
|
|
351
|
|
352 return false, nil
|
|
353 }
|
|
354
|
|
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()
|
|
361 })
|
|
362 if check.err != nil {
|
|
363 t.Helper()
|
|
364 if check.skip {
|
|
365 t.Skip(check.err)
|
|
366 }
|
|
367 t.Fatal(check.err)
|
|
368 }
|
|
369 }
|
|
370
|
|
371 func (c *config) checkRuntime() (skip bool, err error) {
|
|
372 if c.sanitizer != "thread" {
|
|
373 return false, nil
|
|
374 }
|
|
375
|
|
376 // libcgo.h sets CGO_TSAN if it detects TSAN support in the C compiler.
|
145
|
377 // Dump the preprocessor defines to check that works.
|
131
|
378 // (Sometimes it doesn't: see https://golang.org/issue/15983.)
|
|
379 cmd, err := cc(c.cFlags...)
|
|
380 if err != nil {
|
|
381 return false, err
|
|
382 }
|
|
383 cmd.Args = append(cmd.Args, "-dM", "-E", "../../../src/runtime/cgo/libcgo.h")
|
|
384 cmdStr := strings.Join(cmd.Args, " ")
|
|
385 out, err := cmd.CombinedOutput()
|
|
386 if err != nil {
|
|
387 return false, fmt.Errorf("%#q exited with %v\n%s", cmdStr, err, out)
|
|
388 }
|
|
389 if !bytes.Contains(out, []byte("#define CGO_TSAN")) {
|
|
390 return true, fmt.Errorf("%#q did not define CGO_TSAN", cmdStr)
|
|
391 }
|
|
392 return false, nil
|
|
393 }
|
|
394
|
|
395 // srcPath returns the path to the given file relative to this test's source tree.
|
|
396 func srcPath(path string) string {
|
145
|
397 return filepath.Join("testdata", path)
|
131
|
398 }
|
|
399
|
|
400 // A tempDir manages a temporary directory within a test.
|
|
401 type tempDir struct {
|
|
402 base string
|
|
403 }
|
|
404
|
|
405 func (d *tempDir) RemoveAll(t *testing.T) {
|
|
406 t.Helper()
|
|
407 if d.base == "" {
|
|
408 return
|
|
409 }
|
|
410 if err := os.RemoveAll(d.base); err != nil {
|
|
411 t.Fatalf("Failed to remove temp dir: %v", err)
|
|
412 }
|
|
413 }
|
|
414
|
|
415 func (d *tempDir) Join(name string) string {
|
|
416 return filepath.Join(d.base, name)
|
|
417 }
|
|
418
|
|
419 func newTempDir(t *testing.T) *tempDir {
|
|
420 t.Helper()
|
|
421 dir, err := ioutil.TempDir("", filepath.Dir(t.Name()))
|
|
422 if err != nil {
|
|
423 t.Fatalf("Failed to create temp dir: %v", err)
|
|
424 }
|
|
425 return &tempDir{base: dir}
|
|
426 }
|
|
427
|
|
428 // hangProneCmd returns an exec.Cmd for a command that is likely to hang.
|
|
429 //
|
|
430 // If one of these tests hangs, the caller is likely to kill the test process
|
|
431 // using SIGINT, which will be sent to all of the processes in the test's group.
|
|
432 // Unfortunately, TSAN in particular is prone to dropping signals, so the SIGINT
|
|
433 // may terminate the test binary but leave the subprocess running. hangProneCmd
|
|
434 // configures subprocess to receive SIGKILL instead to ensure that it won't
|
|
435 // leak.
|
|
436 func hangProneCmd(name string, arg ...string) *exec.Cmd {
|
|
437 cmd := exec.Command(name, arg...)
|
|
438 cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
439 Pdeathsig: syscall.SIGKILL,
|
|
440 }
|
|
441 return cmd
|
|
442 }
|