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. 7package sanitizers_test 8 9import ( 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 26var overcommit struct { 27 sync.Once 28 value int 29 err error 30} 31 32// requireOvercommit skips t if the kernel does not allow overcommit. 33func 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 53var 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. 60func 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. 83func 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. 91func 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`. 100func 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 154type version struct { 155 name string 156 major, minor int 157} 158 159var 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. 169func 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 221type compilerCheck struct { 222 once sync.Once 223 err error 224 skip bool // If true, skip with err instead of failing with it. 225} 226 227type config struct { 228 sanitizer string 229 230 cFlags, ldFlags, goFlags []string 231 232 sanitizerCheck, runtimeCheck compilerCheck 233} 234 235var configs struct { 236 sync.Mutex 237 m map[string]*config 238} 239 240// configure returns the configuration for the given sanitizer. 241func 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. 283func (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. 294func (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 308var cMain = []byte(` 309int main() { 310 return 0; 311} 312`) 313 314func (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. 356func (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 370func (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. 395func srcPath(path string) string { 396 return filepath.Join("testdata", path) 397} 398 399// A tempDir manages a temporary directory within a test. 400type tempDir struct { 401 base string 402} 403 404func (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 414func (d *tempDir) Join(name string) string { 415 return filepath.Join(d.base, name) 416} 417 418func 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. 435func 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. 445func 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