github.com/epfl-dcsl/gotee@v0.0.0-20200909122901-014b35f5e5e9/misc/cgo/testsanitizers/cc_test.go (about)

     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  }