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