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