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

View as plain text