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	"os"
15	"os/exec"
16	"path/filepath"
17	"regexp"
18	"strconv"
19	"strings"
20	"sync"
21	"syscall"
22	"testing"
23	"unicode"
24)
25
26var overcommit struct {
27	sync.Once
28	value int
29	err   error
30}
31
32// requireOvercommit skips t if the kernel does not allow overcommit.
33func requireOvercommit(t *testing.T) {
34	t.Helper()
35
36	overcommit.Once.Do(func() {
37		var out []byte
38		out, overcommit.err = os.ReadFile("/proc/sys/vm/overcommit_memory")
39		if overcommit.err != nil {
40			return
41		}
42		overcommit.value, overcommit.err = strconv.Atoi(string(bytes.TrimSpace(out)))
43	})
44
45	if overcommit.err != nil {
46		t.Skipf("couldn't determine vm.overcommit_memory (%v); assuming no overcommit", overcommit.err)
47	}
48	if overcommit.value == 2 {
49		t.Skip("vm.overcommit_memory=2")
50	}
51}
52
53var env struct {
54	sync.Once
55	m   map[string]string
56	err error
57}
58
59// goEnv returns the output of $(go env) as a map.
60func goEnv(key string) (string, error) {
61	env.Once.Do(func() {
62		var out []byte
63		out, env.err = exec.Command("go", "env", "-json").Output()
64		if env.err != nil {
65			return
66		}
67
68		env.m = make(map[string]string)
69		env.err = json.Unmarshal(out, &env.m)
70	})
71	if env.err != nil {
72		return "", env.err
73	}
74
75	v, ok := env.m[key]
76	if !ok {
77		return "", fmt.Errorf("`go env`: no entry for %v", key)
78	}
79	return v, nil
80}
81
82// replaceEnv sets the key environment variable to value in cmd.
83func replaceEnv(cmd *exec.Cmd, key, value string) {
84	if cmd.Env == nil {
85		cmd.Env = os.Environ()
86	}
87	cmd.Env = append(cmd.Env, key+"="+value)
88}
89
90// mustRun executes t and fails cmd with a well-formatted message if it fails.
91func mustRun(t *testing.T, cmd *exec.Cmd) {
92	t.Helper()
93	out, err := cmd.CombinedOutput()
94	if err != nil {
95		t.Fatalf("%#q exited with %v\n%s", strings.Join(cmd.Args, " "), err, out)
96	}
97}
98
99// cc returns a cmd that executes `$(go env CC) $(go env GOGCCFLAGS) $args`.
100func cc(args ...string) (*exec.Cmd, error) {
101	CC, err := goEnv("CC")
102	if err != nil {
103		return nil, err
104	}
105
106	GOGCCFLAGS, err := goEnv("GOGCCFLAGS")
107	if err != nil {
108		return nil, err
109	}
110
111	// Split GOGCCFLAGS, respecting quoting.
112	//
113	// TODO(bcmills): This code also appears in
114	// misc/cgo/testcarchive/carchive_test.go, and perhaps ought to go in
115	// src/cmd/dist/test.go as well. Figure out where to put it so that it can be
116	// shared.
117	var flags []string
118	quote := '\000'
119	start := 0
120	lastSpace := true
121	backslash := false
122	for i, c := range GOGCCFLAGS {
123		if quote == '\000' && unicode.IsSpace(c) {
124			if !lastSpace {
125				flags = append(flags, GOGCCFLAGS[start:i])
126				lastSpace = true
127			}
128		} else {
129			if lastSpace {
130				start = i
131				lastSpace = false
132			}
133			if quote == '\000' && !backslash && (c == '"' || c == '\'') {
134				quote = c
135				backslash = false
136			} else if !backslash && quote == c {
137				quote = '\000'
138			} else if (quote == '\000' || quote == '"') && !backslash && c == '\\' {
139				backslash = true
140			} else {
141				backslash = false
142			}
143		}
144	}
145	if !lastSpace {
146		flags = append(flags, GOGCCFLAGS[start:])
147	}
148
149	cmd := exec.Command(CC, flags...)
150	cmd.Args = append(cmd.Args, args...)
151	return cmd, nil
152}
153
154type version struct {
155	name         string
156	major, minor int
157}
158
159var compiler struct {
160	sync.Once
161	version
162	err error
163}
164
165// compilerVersion detects the version of $(go env CC).
166//
167// It returns a non-nil error if the compiler matches a known version schema but
168// the version could not be parsed, or if $(go env CC) could not be determined.
169func compilerVersion() (version, error) {
170	compiler.Once.Do(func() {
171		compiler.err = func() error {
172			compiler.name = "unknown"
173
174			cmd, err := cc("--version")
175			if err != nil {
176				return err
177			}
178			out, err := cmd.Output()
179			if err != nil {
180				// Compiler does not support "--version" flag: not Clang or GCC.
181				return nil
182			}
183
184			var match [][]byte
185			if bytes.HasPrefix(out, []byte("gcc")) {
186				compiler.name = "gcc"
187
188				cmd, err := cc("-dumpversion")
189				if err != nil {
190					return err
191				}
192				out, err := cmd.Output()
193				if err != nil {
194					// gcc, but does not support gcc's "-dumpversion" flag?!
195					return err
196				}
197				gccRE := regexp.MustCompile(`(\d+)\.(\d+)`)
198				match = gccRE.FindSubmatch(out)
199			} else {
200				clangRE := regexp.MustCompile(`clang version (\d+)\.(\d+)`)
201				if match = clangRE.FindSubmatch(out); len(match) > 0 {
202					compiler.name = "clang"
203				}
204			}
205
206			if len(match) < 3 {
207				return nil // "unknown"
208			}
209			if compiler.major, err = strconv.Atoi(string(match[1])); err != nil {
210				return err
211			}
212			if compiler.minor, err = strconv.Atoi(string(match[2])); err != nil {
213				return err
214			}
215			return nil
216		}()
217	})
218	return compiler.version, compiler.err
219}
220
221type compilerCheck struct {
222	once sync.Once
223	err  error
224	skip bool // If true, skip with err instead of failing with it.
225}
226
227type config struct {
228	sanitizer string
229
230	cFlags, ldFlags, goFlags []string
231
232	sanitizerCheck, runtimeCheck compilerCheck
233}
234
235var configs struct {
236	sync.Mutex
237	m map[string]*config
238}
239
240// configure returns the configuration for the given sanitizer.
241func configure(sanitizer string) *config {
242	configs.Lock()
243	defer configs.Unlock()
244	if c, ok := configs.m[sanitizer]; ok {
245		return c
246	}
247
248	c := &config{
249		sanitizer: sanitizer,
250		cFlags:    []string{"-fsanitize=" + sanitizer},
251		ldFlags:   []string{"-fsanitize=" + sanitizer},
252	}
253
254	if testing.Verbose() {
255		c.goFlags = append(c.goFlags, "-x")
256	}
257
258	switch sanitizer {
259	case "memory":
260		c.goFlags = append(c.goFlags, "-msan")
261
262	case "thread":
263		c.goFlags = append(c.goFlags, "--installsuffix=tsan")
264		compiler, _ := compilerVersion()
265		if compiler.name == "gcc" {
266			c.cFlags = append(c.cFlags, "-fPIC")
267			c.ldFlags = append(c.ldFlags, "-fPIC", "-static-libtsan")
268		}
269
270	default:
271		panic(fmt.Sprintf("unrecognized sanitizer: %q", sanitizer))
272	}
273
274	if configs.m == nil {
275		configs.m = make(map[string]*config)
276	}
277	configs.m[sanitizer] = c
278	return c
279}
280
281// goCmd returns a Cmd that executes "go $subcommand $args" with appropriate
282// additional flags and environment.
283func (c *config) goCmd(subcommand string, args ...string) *exec.Cmd {
284	cmd := exec.Command("go", subcommand)
285	cmd.Args = append(cmd.Args, c.goFlags...)
286	cmd.Args = append(cmd.Args, args...)
287	replaceEnv(cmd, "CGO_CFLAGS", strings.Join(c.cFlags, " "))
288	replaceEnv(cmd, "CGO_LDFLAGS", strings.Join(c.ldFlags, " "))
289	return cmd
290}
291
292// skipIfCSanitizerBroken skips t if the C compiler does not produce working
293// binaries as configured.
294func (c *config) skipIfCSanitizerBroken(t *testing.T) {
295	check := &c.sanitizerCheck
296	check.once.Do(func() {
297		check.skip, check.err = c.checkCSanitizer()
298	})
299	if check.err != nil {
300		t.Helper()
301		if check.skip {
302			t.Skip(check.err)
303		}
304		t.Fatal(check.err)
305	}
306}
307
308var cMain = []byte(`
309int main() {
310	return 0;
311}
312`)
313
314func (c *config) checkCSanitizer() (skip bool, err error) {
315	dir, err := os.MkdirTemp("", c.sanitizer)
316	if err != nil {
317		return false, fmt.Errorf("failed to create temp directory: %v", err)
318	}
319	defer os.RemoveAll(dir)
320
321	src := filepath.Join(dir, "return0.c")
322	if err := os.WriteFile(src, cMain, 0600); err != nil {
323		return false, fmt.Errorf("failed to write C source file: %v", err)
324	}
325
326	dst := filepath.Join(dir, "return0")
327	cmd, err := cc(c.cFlags...)
328	if err != nil {
329		return false, err
330	}
331	cmd.Args = append(cmd.Args, c.ldFlags...)
332	cmd.Args = append(cmd.Args, "-o", dst, src)
333	out, err := cmd.CombinedOutput()
334	if err != nil {
335		if bytes.Contains(out, []byte("-fsanitize")) &&
336			(bytes.Contains(out, []byte("unrecognized")) ||
337				bytes.Contains(out, []byte("unsupported"))) {
338			return true, errors.New(string(out))
339		}
340		return true, fmt.Errorf("%#q failed: %v\n%s", strings.Join(cmd.Args, " "), err, out)
341	}
342
343	if out, err := exec.Command(dst).CombinedOutput(); err != nil {
344		if os.IsNotExist(err) {
345			return true, fmt.Errorf("%#q failed to produce executable: %v", strings.Join(cmd.Args, " "), err)
346		}
347		snippet := bytes.SplitN(out, []byte{'\n'}, 2)[0]
348		return true, fmt.Errorf("%#q generated broken executable: %v\n%s", strings.Join(cmd.Args, " "), err, snippet)
349	}
350
351	return false, nil
352}
353
354// skipIfRuntimeIncompatible skips t if the Go runtime is suspected not to work
355// with cgo as configured.
356func (c *config) skipIfRuntimeIncompatible(t *testing.T) {
357	check := &c.runtimeCheck
358	check.once.Do(func() {
359		check.skip, check.err = c.checkRuntime()
360	})
361	if check.err != nil {
362		t.Helper()
363		if check.skip {
364			t.Skip(check.err)
365		}
366		t.Fatal(check.err)
367	}
368}
369
370func (c *config) checkRuntime() (skip bool, err error) {
371	if c.sanitizer != "thread" {
372		return false, nil
373	}
374
375	// libcgo.h sets CGO_TSAN if it detects TSAN support in the C compiler.
376	// Dump the preprocessor defines to check that works.
377	// (Sometimes it doesn't: see https://golang.org/issue/15983.)
378	cmd, err := cc(c.cFlags...)
379	if err != nil {
380		return false, err
381	}
382	cmd.Args = append(cmd.Args, "-dM", "-E", "../../../src/runtime/cgo/libcgo.h")
383	cmdStr := strings.Join(cmd.Args, " ")
384	out, err := cmd.CombinedOutput()
385	if err != nil {
386		return false, fmt.Errorf("%#q exited with %v\n%s", cmdStr, err, out)
387	}
388	if !bytes.Contains(out, []byte("#define CGO_TSAN")) {
389		return true, fmt.Errorf("%#q did not define CGO_TSAN", cmdStr)
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("testdata", 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 := os.MkdirTemp("", 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
443// mSanSupported is a copy of the function cmd/internal/sys.MSanSupported,
444// because the internal pacakage can't be used here.
445func mSanSupported(goos, goarch string) bool {
446	switch goos {
447	case "linux":
448		return goarch == "amd64" || goarch == "arm64"
449	default:
450		return false
451	}
452}
453