Source file misc/cgo/testsanitizers/cc_test.go

     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  	"os"
    15  	"os/exec"
    16  	"path/filepath"
    17  	"regexp"
    18  	"strconv"
    19  	"strings"
    20  	"sync"
    21  	"syscall"
    22  	"testing"
    23  	"unicode"
    24  )
    25  
    26  var overcommit struct {
    27  	sync.Once
    28  	value int
    29  	err   error
    30  }
    31  
    32  // requireOvercommit skips t if the kernel does not allow overcommit.
    33  func 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  
    53  var 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.
    60  func 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.
    83  func 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.
    91  func 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`.
   100  func 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  
   154  type version struct {
   155  	name         string
   156  	major, minor int
   157  }
   158  
   159  var 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.
   169  func 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  
   221  type compilerCheck struct {
   222  	once sync.Once
   223  	err  error
   224  	skip bool // If true, skip with err instead of failing with it.
   225  }
   226  
   227  type config struct {
   228  	sanitizer string
   229  
   230  	cFlags, ldFlags, goFlags []string
   231  
   232  	sanitizerCheck, runtimeCheck compilerCheck
   233  }
   234  
   235  var configs struct {
   236  	sync.Mutex
   237  	m map[string]*config
   238  }
   239  
   240  // configure returns the configuration for the given sanitizer.
   241  func 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.
   283  func (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.
   294  func (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  
   308  var cMain = []byte(`
   309  int main() {
   310  	return 0;
   311  }
   312  `)
   313  
   314  func (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.
   356  func (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  
   370  func (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.
   395  func srcPath(path string) string {
   396  	return filepath.Join("testdata", 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 := 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.
   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  
   443  // mSanSupported is a copy of the function cmd/internal/sys.MSanSupported,
   444  // because the internal pacakage can't be used here.
   445  func 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  

View as plain text