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	case "address":
271		c.goFlags = append(c.goFlags, "-asan")
272
273	default:
274		panic(fmt.Sprintf("unrecognized sanitizer: %q", sanitizer))
275	}
276
277	if configs.m == nil {
278		configs.m = make(map[string]*config)
279	}
280	configs.m[sanitizer] = c
281	return c
282}
283
284// goCmd returns a Cmd that executes "go $subcommand $args" with appropriate
285// additional flags and environment.
286func (c *config) goCmd(subcommand string, args ...string) *exec.Cmd {
287	cmd := exec.Command("go", subcommand)
288	cmd.Args = append(cmd.Args, c.goFlags...)
289	cmd.Args = append(cmd.Args, args...)
290	replaceEnv(cmd, "CGO_CFLAGS", strings.Join(c.cFlags, " "))
291	replaceEnv(cmd, "CGO_LDFLAGS", strings.Join(c.ldFlags, " "))
292	return cmd
293}
294
295// skipIfCSanitizerBroken skips t if the C compiler does not produce working
296// binaries as configured.
297func (c *config) skipIfCSanitizerBroken(t *testing.T) {
298	check := &c.sanitizerCheck
299	check.once.Do(func() {
300		check.skip, check.err = c.checkCSanitizer()
301	})
302	if check.err != nil {
303		t.Helper()
304		if check.skip {
305			t.Skip(check.err)
306		}
307		t.Fatal(check.err)
308	}
309}
310
311var cMain = []byte(`
312int main() {
313	return 0;
314}
315`)
316
317func (c *config) checkCSanitizer() (skip bool, err error) {
318	dir, err := os.MkdirTemp("", c.sanitizer)
319	if err != nil {
320		return false, fmt.Errorf("failed to create temp directory: %v", err)
321	}
322	defer os.RemoveAll(dir)
323
324	src := filepath.Join(dir, "return0.c")
325	if err := os.WriteFile(src, cMain, 0600); err != nil {
326		return false, fmt.Errorf("failed to write C source file: %v", err)
327	}
328
329	dst := filepath.Join(dir, "return0")
330	cmd, err := cc(c.cFlags...)
331	if err != nil {
332		return false, err
333	}
334	cmd.Args = append(cmd.Args, c.ldFlags...)
335	cmd.Args = append(cmd.Args, "-o", dst, src)
336	out, err := cmd.CombinedOutput()
337	if err != nil {
338		if bytes.Contains(out, []byte("-fsanitize")) &&
339			(bytes.Contains(out, []byte("unrecognized")) ||
340				bytes.Contains(out, []byte("unsupported"))) {
341			return true, errors.New(string(out))
342		}
343		return true, fmt.Errorf("%#q failed: %v\n%s", strings.Join(cmd.Args, " "), err, out)
344	}
345
346	if out, err := exec.Command(dst).CombinedOutput(); err != nil {
347		if os.IsNotExist(err) {
348			return true, fmt.Errorf("%#q failed to produce executable: %v", strings.Join(cmd.Args, " "), err)
349		}
350		snippet, _, _ := bytes.Cut(out, []byte("\n"))
351		return true, fmt.Errorf("%#q generated broken executable: %v\n%s", strings.Join(cmd.Args, " "), err, snippet)
352	}
353
354	return false, nil
355}
356
357// skipIfRuntimeIncompatible skips t if the Go runtime is suspected not to work
358// with cgo as configured.
359func (c *config) skipIfRuntimeIncompatible(t *testing.T) {
360	check := &c.runtimeCheck
361	check.once.Do(func() {
362		check.skip, check.err = c.checkRuntime()
363	})
364	if check.err != nil {
365		t.Helper()
366		if check.skip {
367			t.Skip(check.err)
368		}
369		t.Fatal(check.err)
370	}
371}
372
373func (c *config) checkRuntime() (skip bool, err error) {
374	if c.sanitizer != "thread" {
375		return false, nil
376	}
377
378	// libcgo.h sets CGO_TSAN if it detects TSAN support in the C compiler.
379	// Dump the preprocessor defines to check that works.
380	// (Sometimes it doesn't: see https://golang.org/issue/15983.)
381	cmd, err := cc(c.cFlags...)
382	if err != nil {
383		return false, err
384	}
385	cmd.Args = append(cmd.Args, "-dM", "-E", "../../../src/runtime/cgo/libcgo.h")
386	cmdStr := strings.Join(cmd.Args, " ")
387	out, err := cmd.CombinedOutput()
388	if err != nil {
389		return false, fmt.Errorf("%#q exited with %v\n%s", cmdStr, err, out)
390	}
391	if !bytes.Contains(out, []byte("#define CGO_TSAN")) {
392		return true, fmt.Errorf("%#q did not define CGO_TSAN", cmdStr)
393	}
394	return false, nil
395}
396
397// srcPath returns the path to the given file relative to this test's source tree.
398func srcPath(path string) string {
399	return filepath.Join("testdata", path)
400}
401
402// A tempDir manages a temporary directory within a test.
403type tempDir struct {
404	base string
405}
406
407func (d *tempDir) RemoveAll(t *testing.T) {
408	t.Helper()
409	if d.base == "" {
410		return
411	}
412	if err := os.RemoveAll(d.base); err != nil {
413		t.Fatalf("Failed to remove temp dir: %v", err)
414	}
415}
416
417func (d *tempDir) Join(name string) string {
418	return filepath.Join(d.base, name)
419}
420
421func newTempDir(t *testing.T) *tempDir {
422	t.Helper()
423	dir, err := os.MkdirTemp("", filepath.Dir(t.Name()))
424	if err != nil {
425		t.Fatalf("Failed to create temp dir: %v", err)
426	}
427	return &tempDir{base: dir}
428}
429
430// hangProneCmd returns an exec.Cmd for a command that is likely to hang.
431//
432// If one of these tests hangs, the caller is likely to kill the test process
433// using SIGINT, which will be sent to all of the processes in the test's group.
434// Unfortunately, TSAN in particular is prone to dropping signals, so the SIGINT
435// may terminate the test binary but leave the subprocess running. hangProneCmd
436// configures subprocess to receive SIGKILL instead to ensure that it won't
437// leak.
438func hangProneCmd(name string, arg ...string) *exec.Cmd {
439	cmd := exec.Command(name, arg...)
440	cmd.SysProcAttr = &syscall.SysProcAttr{
441		Pdeathsig: syscall.SIGKILL,
442	}
443	return cmd
444}
445
446// mSanSupported is a copy of the function cmd/internal/sys.MSanSupported,
447// because the internal pacakage can't be used here.
448func mSanSupported(goos, goarch string) bool {
449	switch goos {
450	case "linux":
451		return goarch == "amd64" || goarch == "arm64"
452	default:
453		return false
454	}
455}
456
457// aSanSupported is a copy of the function cmd/internal/sys.ASanSupported,
458// because the internal pacakage can't be used here.
459func aSanSupported(goos, goarch string) bool {
460	switch goos {
461	case "linux":
462		return goarch == "amd64" || goarch == "arm64"
463	default:
464		return false
465	}
466}
467