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.
7package sanitizers_test
8
9import (
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
27var overcommit struct {
28	sync.Once
29	value int
30	err   error
31}
32
33// requireOvercommit skips t if the kernel does not allow overcommit.
34func 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
54var 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.
61func 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.
84func 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.
92func 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`.
101func 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
155type version struct {
156	name         string
157	major, minor int
158}
159
160var 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.
170func 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
222type compilerCheck struct {
223	once sync.Once
224	err  error
225	skip bool // If true, skip with err instead of failing with it.
226}
227
228type config struct {
229	sanitizer string
230
231	cFlags, ldFlags, goFlags []string
232
233	sanitizerCheck, runtimeCheck compilerCheck
234}
235
236var configs struct {
237	sync.Mutex
238	m map[string]*config
239}
240
241// configure returns the configuration for the given sanitizer.
242func 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.
284func (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.
295func (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
309var cMain = []byte(`
310int main() {
311	return 0;
312}
313`)
314
315func (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.
357func (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
371func (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.
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...)
380	if err != nil {
381		return false, err
382	}
383	cmd.Args = append(cmd.Args, "-dM", "-E", "../../../src/runtime/cgo/libcgo.h")
384	out, err := cmd.CombinedOutput()
385	if err != nil {
386		return false, fmt.Errorf("%#q exited with %v\n%s", strings.Join(cmd.Args, " "), err, out)
387	}
388	if !bytes.Contains(out, []byte("#define CGO_TSAN")) {
389		return true, fmt.Errorf("%#q did not define CGO_TSAN")
390	}
391	return false, nil
392}
393
394// srcPath returns the path to the given file relative to this test's source tree.
395func srcPath(path string) string {
396	return filepath.Join("src", path)
397}
398
399// A tempDir manages a temporary directory within a test.
400type tempDir struct {
401	base string
402}
403
404func (d *tempDir) RemoveAll(t *testing.T) {
405	t.Helper()
406	if d.base == "" {
407		return
408	}
409	if err := os.RemoveAll(d.base); err != nil {
410		t.Fatalf("Failed to remove temp dir: %v", err)
411	}
412}
413
414func (d *tempDir) Join(name string) string {
415	return filepath.Join(d.base, name)
416}
417
418func newTempDir(t *testing.T) *tempDir {
419	t.Helper()
420	dir, err := ioutil.TempDir("", filepath.Dir(t.Name()))
421	if err != nil {
422		t.Fatalf("Failed to create temp dir: %v", err)
423	}
424	return &tempDir{base: dir}
425}
426
427// hangProneCmd returns an exec.Cmd for a command that is likely to hang.
428//
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
434// leak.
435func hangProneCmd(name string, arg ...string) *exec.Cmd {
436	cmd := exec.Command(name, arg...)
437	cmd.SysProcAttr = &syscall.SysProcAttr{
438		Pdeathsig: syscall.SIGKILL,
439	}
440	return cmd
441}
442