Source file src/cmd/cgo/internal/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  // This test uses the Pdeathsig field of syscall.SysProcAttr, so it only works
     6  // on platforms that support that.
     7  
     8  //go:build linux || (freebsd && amd64)
     9  
    10  // sanitizers_test checks the use of Go with sanitizers like msan, asan, etc.
    11  // See https://github.com/google/sanitizers.
    12  package sanitizers_test
    13  
    14  import (
    15  	"bytes"
    16  	"context"
    17  	"encoding/json"
    18  	"errors"
    19  	"fmt"
    20  	"internal/testenv"
    21  	"os"
    22  	"os/exec"
    23  	"os/user"
    24  	"path/filepath"
    25  	"regexp"
    26  	"strconv"
    27  	"strings"
    28  	"sync"
    29  	"syscall"
    30  	"testing"
    31  	"time"
    32  	"unicode"
    33  )
    34  
    35  var overcommit struct {
    36  	sync.Once
    37  	value int
    38  	err   error
    39  }
    40  
    41  // requireOvercommit skips t if the kernel does not allow overcommit.
    42  func requireOvercommit(t *testing.T) {
    43  	t.Helper()
    44  
    45  	overcommit.Once.Do(func() {
    46  		var out []byte
    47  		out, overcommit.err = os.ReadFile("/proc/sys/vm/overcommit_memory")
    48  		if overcommit.err != nil {
    49  			return
    50  		}
    51  		overcommit.value, overcommit.err = strconv.Atoi(string(bytes.TrimSpace(out)))
    52  	})
    53  
    54  	if overcommit.err != nil {
    55  		t.Skipf("couldn't determine vm.overcommit_memory (%v); assuming no overcommit", overcommit.err)
    56  	}
    57  	if overcommit.value == 2 {
    58  		t.Skip("vm.overcommit_memory=2")
    59  	}
    60  }
    61  
    62  var env struct {
    63  	sync.Once
    64  	m   map[string]string
    65  	err error
    66  }
    67  
    68  // goEnv returns the output of $(go env) as a map.
    69  func goEnv(key string) (string, error) {
    70  	env.Once.Do(func() {
    71  		var out []byte
    72  		out, env.err = exec.Command("go", "env", "-json").Output()
    73  		if env.err != nil {
    74  			return
    75  		}
    76  
    77  		env.m = make(map[string]string)
    78  		env.err = json.Unmarshal(out, &env.m)
    79  	})
    80  	if env.err != nil {
    81  		return "", env.err
    82  	}
    83  
    84  	v, ok := env.m[key]
    85  	if !ok {
    86  		return "", fmt.Errorf("`go env`: no entry for %v", key)
    87  	}
    88  	return v, nil
    89  }
    90  
    91  // replaceEnv sets the key environment variable to value in cmd.
    92  func replaceEnv(cmd *exec.Cmd, key, value string) {
    93  	if cmd.Env == nil {
    94  		cmd.Env = cmd.Environ()
    95  	}
    96  	cmd.Env = append(cmd.Env, key+"="+value)
    97  }
    98  
    99  // appendExperimentEnv appends comma-separated experiments to GOEXPERIMENT.
   100  func appendExperimentEnv(cmd *exec.Cmd, experiments []string) {
   101  	if cmd.Env == nil {
   102  		cmd.Env = cmd.Environ()
   103  	}
   104  	exps := strings.Join(experiments, ",")
   105  	for _, evar := range cmd.Env {
   106  		c := strings.SplitN(evar, "=", 2)
   107  		if c[0] == "GOEXPERIMENT" {
   108  			exps = c[1] + "," + exps
   109  		}
   110  	}
   111  	cmd.Env = append(cmd.Env, "GOEXPERIMENT="+exps)
   112  }
   113  
   114  func appendASANOptions(cmd *exec.Cmd, opts ...string) {
   115  	if cmd.Env == nil {
   116  		cmd.Env = cmd.Environ()
   117  	}
   118  	var asanOptions string
   119  	for _, evar := range cmd.Env {
   120  		name, value, ok := strings.Cut(evar, "=")
   121  		if ok && name == "ASAN_OPTIONS" {
   122  			asanOptions = value
   123  		}
   124  	}
   125  	if asanOptions != "" {
   126  		asanOptions += ":"
   127  	}
   128  	asanOptions += strings.Join(opts, ":")
   129  	cmd.Env = append(cmd.Env, "ASAN_OPTIONS="+asanOptions)
   130  }
   131  
   132  // mustRun executes t and fails cmd with a well-formatted message if it fails.
   133  func mustRun(t *testing.T, cmd *exec.Cmd) {
   134  	t.Helper()
   135  	out := new(strings.Builder)
   136  	cmd.Stdout = out
   137  	cmd.Stderr = out
   138  
   139  	err := cmd.Start()
   140  	if err != nil {
   141  		t.Fatalf("%v: %v", cmd, err)
   142  	}
   143  
   144  	if deadline, ok := t.Deadline(); ok {
   145  		timeout := time.Until(deadline)
   146  		timeout -= timeout / 10 // Leave 10% headroom for logging and cleanup.
   147  		timer := time.AfterFunc(timeout, func() {
   148  			cmd.Process.Signal(syscall.SIGQUIT)
   149  		})
   150  		defer timer.Stop()
   151  	}
   152  
   153  	if err := cmd.Wait(); err != nil {
   154  		t.Fatalf("%v exited with %v\n%s", cmd, err, out)
   155  	}
   156  }
   157  
   158  // cc returns a cmd that executes `$(go env CC) $(go env GOGCCFLAGS) $args`.
   159  func cc(ctx context.Context, args ...string) (*exec.Cmd, error) {
   160  	CC, err := goEnv("CC")
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  
   165  	GOGCCFLAGS, err := goEnv("GOGCCFLAGS")
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  
   170  	// Split GOGCCFLAGS, respecting quoting.
   171  	//
   172  	// TODO(bcmills): This code also appears in
   173  	// cmd/cgo/internal/testcarchive/carchive_test.go, and perhaps ought to go in
   174  	// src/cmd/dist/test.go as well. Figure out where to put it so that it can be
   175  	// shared.
   176  	var flags []string
   177  	quote := '\000'
   178  	start := 0
   179  	lastSpace := true
   180  	backslash := false
   181  	for i, c := range GOGCCFLAGS {
   182  		if quote == '\000' && unicode.IsSpace(c) {
   183  			if !lastSpace {
   184  				flags = append(flags, GOGCCFLAGS[start:i])
   185  				lastSpace = true
   186  			}
   187  		} else {
   188  			if lastSpace {
   189  				start = i
   190  				lastSpace = false
   191  			}
   192  			if quote == '\000' && !backslash && (c == '"' || c == '\'') {
   193  				quote = c
   194  				backslash = false
   195  			} else if !backslash && quote == c {
   196  				quote = '\000'
   197  			} else if (quote == '\000' || quote == '"') && !backslash && c == '\\' {
   198  				backslash = true
   199  			} else {
   200  				backslash = false
   201  			}
   202  		}
   203  	}
   204  	if !lastSpace {
   205  		flags = append(flags, GOGCCFLAGS[start:])
   206  	}
   207  
   208  	cmd := exec.CommandContext(ctx, CC, flags...)
   209  	cmd.Args = append(cmd.Args, args...)
   210  	return cmd, nil
   211  }
   212  
   213  type version struct {
   214  	name         string
   215  	major, minor int
   216  }
   217  
   218  var compiler struct {
   219  	sync.Once
   220  	version
   221  	err error
   222  }
   223  
   224  // compilerVersion detects the version of $(go env CC).
   225  //
   226  // It returns a non-nil error if the compiler matches a known version schema but
   227  // the version could not be parsed, or if $(go env CC) could not be determined.
   228  func compilerVersion() (version, error) {
   229  	compiler.Once.Do(func() {
   230  		compiler.err = func() error {
   231  			compiler.name = "unknown"
   232  
   233  			cmd, err := cc(context.Background(), "--version")
   234  			if err != nil {
   235  				return err
   236  			}
   237  			out, err := cmd.Output()
   238  			if err != nil {
   239  				// Compiler does not support "--version" flag: not Clang or GCC.
   240  				return nil
   241  			}
   242  
   243  			var match [][]byte
   244  			if bytes.HasPrefix(out, []byte("gcc")) {
   245  				compiler.name = "gcc"
   246  				cmd, err := cc(context.Background(), "-dumpfullversion", "-dumpversion")
   247  				if err != nil {
   248  					return err
   249  				}
   250  				out, err := cmd.Output()
   251  				if err != nil {
   252  					// gcc, but does not support gcc's "-v" flag?!
   253  					return err
   254  				}
   255  				gccRE := regexp.MustCompile(`(\d+)\.(\d+)`)
   256  				match = gccRE.FindSubmatch(out)
   257  			} else {
   258  				clangRE := regexp.MustCompile(`clang version (\d+)\.(\d+)`)
   259  				if match = clangRE.FindSubmatch(out); len(match) > 0 {
   260  					compiler.name = "clang"
   261  				}
   262  			}
   263  
   264  			if len(match) < 3 {
   265  				return nil // "unknown"
   266  			}
   267  			if compiler.major, err = strconv.Atoi(string(match[1])); err != nil {
   268  				return err
   269  			}
   270  			if compiler.minor, err = strconv.Atoi(string(match[2])); err != nil {
   271  				return err
   272  			}
   273  			return nil
   274  		}()
   275  	})
   276  	return compiler.version, compiler.err
   277  }
   278  
   279  // compilerSupportsLocation reports whether the compiler should be
   280  // able to provide file/line information in backtraces.
   281  func compilerSupportsLocation() bool {
   282  	compiler, err := compilerVersion()
   283  	if err != nil {
   284  		return false
   285  	}
   286  	switch compiler.name {
   287  	case "gcc":
   288  		// TODO(72752): the asan runtime support library
   289  		// (libasan.so.6) shipped with GCC 10 has problems digesting
   290  		// version 5 DWARF produced by the Go toolchain. Disable
   291  		// location checking if gcc is not sufficiently up to date in
   292  		// this case.
   293  		return compiler.major > 10
   294  	case "clang":
   295  		// TODO(65606): The clang toolchain on the LUCI builders is not built against
   296  		// zlib, the ASAN runtime can't actually symbolize its own stack trace. Once
   297  		// this is resolved, one way or another, switch this back to 'true'. We still
   298  		// have coverage from the 'gcc' case above.
   299  		if inLUCIBuild() {
   300  			return false
   301  		}
   302  		return true
   303  	default:
   304  		return false
   305  	}
   306  }
   307  
   308  // inLUCIBuild returns true if we're currently executing in a LUCI build.
   309  func inLUCIBuild() bool {
   310  	u, err := user.Current()
   311  	if err != nil {
   312  		return false
   313  	}
   314  	return testenv.Builder() != "" && u.Username == "swarming"
   315  }
   316  
   317  // compilerRequiredTsanVersion reports whether the compiler is the version required by Tsan.
   318  // Only restrictions for ppc64le are known; otherwise return true.
   319  func compilerRequiredTsanVersion(goos, goarch string) bool {
   320  	compiler, err := compilerVersion()
   321  	if err != nil {
   322  		return false
   323  	}
   324  	if compiler.name == "gcc" && goarch == "ppc64le" {
   325  		return compiler.major >= 9
   326  	}
   327  	return true
   328  }
   329  
   330  // compilerRequiredAsanVersion reports whether the compiler is the version required by Asan.
   331  func compilerRequiredAsanVersion(goos, goarch string) bool {
   332  	compiler, err := compilerVersion()
   333  	if err != nil {
   334  		return false
   335  	}
   336  	switch compiler.name {
   337  	case "gcc":
   338  		if goarch == "loong64" {
   339  			return compiler.major >= 14
   340  		}
   341  		if goarch == "ppc64le" {
   342  			return compiler.major >= 9
   343  		}
   344  		return compiler.major >= 7
   345  	case "clang":
   346  		if goarch == "loong64" {
   347  			return compiler.major >= 16
   348  		}
   349  		return compiler.major >= 9
   350  	default:
   351  		return false
   352  	}
   353  }
   354  
   355  // compilerRequiredLsanVersion reports whether the compiler is the
   356  // version required by Lsan.
   357  func compilerRequiredLsanVersion(goos, goarch string) bool {
   358  	return compilerRequiredAsanVersion(goos, goarch)
   359  }
   360  
   361  type compilerCheck struct {
   362  	once sync.Once
   363  	err  error
   364  	skip bool // If true, skip with err instead of failing with it.
   365  }
   366  
   367  type config struct {
   368  	sanitizer string
   369  
   370  	cFlags, ldFlags, goFlags []string
   371  
   372  	sanitizerCheck, runtimeCheck compilerCheck
   373  }
   374  
   375  var configs struct {
   376  	sync.Mutex
   377  	m map[string]*config
   378  }
   379  
   380  // configure returns the configuration for the given sanitizer.
   381  func configure(sanitizer string) *config {
   382  	configs.Lock()
   383  	defer configs.Unlock()
   384  	if c, ok := configs.m[sanitizer]; ok {
   385  		return c
   386  	}
   387  
   388  	sanitizerOpt := sanitizer
   389  	// For the leak detector, we use "go build -asan",
   390  	// which implies the address sanitizer.
   391  	// We may want to adjust this someday.
   392  	if sanitizer == "leak" {
   393  		sanitizerOpt = "address"
   394  	}
   395  
   396  	c := &config{
   397  		sanitizer: sanitizer,
   398  		cFlags:    []string{"-fsanitize=" + sanitizerOpt},
   399  		ldFlags:   []string{"-fsanitize=" + sanitizerOpt},
   400  	}
   401  
   402  	if testing.Verbose() {
   403  		c.goFlags = append(c.goFlags, "-x")
   404  	}
   405  
   406  	switch sanitizer {
   407  	case "memory":
   408  		c.goFlags = append(c.goFlags, "-msan")
   409  
   410  	case "thread":
   411  		c.goFlags = append(c.goFlags, "--installsuffix=tsan")
   412  		compiler, _ := compilerVersion()
   413  		if compiler.name == "gcc" {
   414  			c.cFlags = append(c.cFlags, "-fPIC")
   415  			c.ldFlags = append(c.ldFlags, "-fPIC", "-static-libtsan")
   416  		}
   417  
   418  	case "address", "leak":
   419  		c.goFlags = append(c.goFlags, "-asan")
   420  		// Set the debug mode to print the C stack trace.
   421  		c.cFlags = append(c.cFlags, "-g")
   422  
   423  	case "fuzzer":
   424  		c.goFlags = append(c.goFlags, "-tags=libfuzzer", "-gcflags=-d=libfuzzer")
   425  
   426  	default:
   427  		panic(fmt.Sprintf("unrecognized sanitizer: %q", sanitizer))
   428  	}
   429  
   430  	if configs.m == nil {
   431  		configs.m = make(map[string]*config)
   432  	}
   433  	configs.m[sanitizer] = c
   434  	return c
   435  }
   436  
   437  // goCmd returns a Cmd that executes "go $subcommand $args" with appropriate
   438  // additional flags and environment.
   439  func (c *config) goCmd(subcommand string, args ...string) *exec.Cmd {
   440  	return c.goCmdWithExperiments(subcommand, args, nil)
   441  }
   442  
   443  // goCmdWithExperiments returns a Cmd that executes
   444  // "GOEXPERIMENT=$experiments go $subcommand $args" with appropriate
   445  // additional flags and CGO-related environment variables.
   446  func (c *config) goCmdWithExperiments(subcommand string, args []string, experiments []string) *exec.Cmd {
   447  	cmd := exec.Command("go", subcommand)
   448  	cmd.Args = append(cmd.Args, c.goFlags...)
   449  	cmd.Args = append(cmd.Args, args...)
   450  	replaceEnv(cmd, "CGO_CFLAGS", strings.Join(c.cFlags, " "))
   451  	replaceEnv(cmd, "CGO_LDFLAGS", strings.Join(c.ldFlags, " "))
   452  	appendExperimentEnv(cmd, experiments)
   453  	return cmd
   454  }
   455  
   456  // skipIfCSanitizerBroken skips t if the C compiler does not produce working
   457  // binaries as configured.
   458  func (c *config) skipIfCSanitizerBroken(t *testing.T) {
   459  	check := &c.sanitizerCheck
   460  	check.once.Do(func() {
   461  		check.skip, check.err = c.checkCSanitizer()
   462  	})
   463  	if check.err != nil {
   464  		t.Helper()
   465  		if check.skip {
   466  			t.Skip(check.err)
   467  		}
   468  		t.Fatal(check.err)
   469  	}
   470  }
   471  
   472  var cMain = []byte(`
   473  int main() {
   474  	return 0;
   475  }
   476  `)
   477  
   478  var cLibFuzzerInput = []byte(`
   479  #include <stddef.h>
   480  int LLVMFuzzerTestOneInput(char *data, size_t size) {
   481  	return 0;
   482  }
   483  `)
   484  
   485  func (c *config) checkCSanitizer() (skip bool, err error) {
   486  	// The sanitizer probes compile and run tiny C programs. If either step
   487  	// takes longer than this, treat the C sanitizer configuration as broken
   488  	// instead of letting the package-level test timeout fire.
   489  	probeTimeout := 20 * time.Second
   490  
   491  	dir, err := os.MkdirTemp("", c.sanitizer)
   492  	if err != nil {
   493  		return false, fmt.Errorf("failed to create temp directory: %v", err)
   494  	}
   495  	defer os.RemoveAll(dir)
   496  
   497  	src := filepath.Join(dir, "return0.c")
   498  	cInput := cMain
   499  	if c.sanitizer == "fuzzer" {
   500  		// libFuzzer generates the main function itself, and uses a different input.
   501  		cInput = cLibFuzzerInput
   502  	}
   503  	if err := os.WriteFile(src, cInput, 0600); err != nil {
   504  		return false, fmt.Errorf("failed to write C source file: %v", err)
   505  	}
   506  
   507  	dst := filepath.Join(dir, "return0")
   508  	compileCtx, cancelCompile := context.WithTimeout(context.Background(), probeTimeout)
   509  	defer cancelCompile()
   510  	cmd, err := cc(compileCtx, c.cFlags...)
   511  	if err != nil {
   512  		return false, err
   513  	}
   514  	cmd.Args = append(cmd.Args, c.ldFlags...)
   515  	cmd.Args = append(cmd.Args, "-o", dst, src)
   516  	if c.sanitizer == "address" {
   517  		// This is only a compiler support probe for ASAN. Some libasan versions
   518  		// run a slow LeakSanitizer check at exit even for this no-op C program,
   519  		// which can hang TestASAN before it starts testing Go binaries.
   520  		appendASANOptions(cmd, "leak_check_at_exit=0")
   521  	}
   522  	makeHangProne(cmd)
   523  	out, err := cmd.CombinedOutput()
   524  	if err != nil {
   525  		if errors.Is(compileCtx.Err(), context.DeadlineExceeded) {
   526  			return true, fmt.Errorf("%#q timed out after %v", cmd, probeTimeout)
   527  		}
   528  		if bytes.Contains(out, []byte("-fsanitize")) &&
   529  			(bytes.Contains(out, []byte("unrecognized")) ||
   530  				bytes.Contains(out, []byte("unsupported"))) {
   531  			return true, errors.New(string(out))
   532  		}
   533  		return true, fmt.Errorf("%#q failed: %v\n%s", cmd, err, out)
   534  	}
   535  
   536  	if c.sanitizer == "fuzzer" {
   537  		// For fuzzer, don't try running the test binary. It never finishes.
   538  		return false, nil
   539  	}
   540  
   541  	runCtx, cancelRun := context.WithTimeout(context.Background(), probeTimeout)
   542  	defer cancelRun()
   543  	cmd = exec.CommandContext(runCtx, dst)
   544  	makeHangProne(cmd)
   545  	if c.sanitizer == "address" {
   546  		// Match the compile-time probe above: avoid libasan's implicit LSan exit
   547  		// scan for this standalone C binary. The explicit LSAN tests still run
   548  		// with leak checking enabled.
   549  		appendASANOptions(cmd, "leak_check_at_exit=0")
   550  	}
   551  	if out, err := cmd.CombinedOutput(); err != nil {
   552  		if errors.Is(runCtx.Err(), context.DeadlineExceeded) {
   553  			return true, fmt.Errorf("%#q timed out after %v", cmd, probeTimeout)
   554  		}
   555  		if os.IsNotExist(err) {
   556  			return true, fmt.Errorf("%#q failed to produce executable: %v", cmd, err)
   557  		}
   558  		snippet, _, _ := bytes.Cut(out, []byte("\n"))
   559  		return true, fmt.Errorf("%#q generated broken executable: %v\n%s", cmd, err, snippet)
   560  	}
   561  
   562  	return false, nil
   563  }
   564  
   565  // skipIfRuntimeIncompatible skips t if the Go runtime is suspected not to work
   566  // with cgo as configured.
   567  func (c *config) skipIfRuntimeIncompatible(t *testing.T) {
   568  	check := &c.runtimeCheck
   569  	check.once.Do(func() {
   570  		check.skip, check.err = c.checkRuntime()
   571  	})
   572  	if check.err != nil {
   573  		t.Helper()
   574  		if check.skip {
   575  			t.Skip(check.err)
   576  		}
   577  		t.Fatal(check.err)
   578  	}
   579  }
   580  
   581  func (c *config) checkRuntime() (skip bool, err error) {
   582  	if c.sanitizer != "thread" {
   583  		return false, nil
   584  	}
   585  
   586  	// libcgo.h sets CGO_TSAN if it detects TSAN support in the C compiler.
   587  	// Dump the preprocessor defines to check that works.
   588  	// (Sometimes it doesn't: see https://golang.org/issue/15983.)
   589  	cmd, err := cc(context.Background(), c.cFlags...)
   590  	if err != nil {
   591  		return false, err
   592  	}
   593  	cmd.Args = append(cmd.Args, "-dM", "-E", "../../../../runtime/cgo/libcgo.h")
   594  	out, err := cmd.CombinedOutput()
   595  	if err != nil {
   596  		return false, fmt.Errorf("%#q exited with %v\n%s", cmd, err, out)
   597  	}
   598  	if !bytes.Contains(out, []byte("#define CGO_TSAN")) {
   599  		return true, fmt.Errorf("%#q did not define CGO_TSAN", cmd)
   600  	}
   601  	return false, nil
   602  }
   603  
   604  // srcPath returns the path to the given file relative to this test's source tree.
   605  func srcPath(path string) string {
   606  	return "./testdata/" + path
   607  }
   608  
   609  // A tempDir manages a temporary directory within a test.
   610  type tempDir struct {
   611  	base string
   612  }
   613  
   614  func (d *tempDir) RemoveAll(t *testing.T) {
   615  	t.Helper()
   616  	if d.base == "" {
   617  		return
   618  	}
   619  	if err := os.RemoveAll(d.base); err != nil {
   620  		t.Fatalf("Failed to remove temp dir: %v", err)
   621  	}
   622  }
   623  
   624  func (d *tempDir) Base() string {
   625  	return d.base
   626  }
   627  
   628  func (d *tempDir) Join(name string) string {
   629  	return filepath.Join(d.base, name)
   630  }
   631  
   632  func newTempDir(t *testing.T) *tempDir {
   633  	return &tempDir{base: t.TempDir()}
   634  }
   635  
   636  // hangProneCmd returns an exec.Cmd for a command that is likely to hang.
   637  //
   638  // If one of these tests hangs, the caller is likely to kill the test process
   639  // using SIGINT, which will be sent to all of the processes in the test's group.
   640  // Unfortunately, TSAN in particular is prone to dropping signals, so the SIGINT
   641  // may terminate the test binary but leave the subprocess running. hangProneCmd
   642  // configures subprocess to receive SIGKILL instead to ensure that it won't
   643  // leak.
   644  func hangProneCmd(name string, arg ...string) *exec.Cmd {
   645  	cmd := exec.Command(name, arg...)
   646  	makeHangProne(cmd)
   647  	return cmd
   648  }
   649  
   650  // makeHangProne configures cmd to receive SIGKILL when the parent process receives SIGINT.
   651  // See [hangProneCmd] for details.
   652  func makeHangProne(cmd *exec.Cmd) {
   653  	cmd.SysProcAttr = &syscall.SysProcAttr{
   654  		Pdeathsig: syscall.SIGKILL,
   655  	}
   656  }
   657  

View as plain text