Home | History | Annotate | Download | only in testsanitizers
      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.
    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.
    395 func srcPath(path string) string {
    396 	return filepath.Join("src", path)
    397 }
    398 
    399 // A tempDir manages a temporary directory within a test.
    400 type tempDir struct {
    401 	base string
    402 }
    403 
    404 func (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 
    414 func (d *tempDir) Join(name string) string {
    415 	return filepath.Join(d.base, name)
    416 }
    417 
    418 func 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.
    435 func 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