1
2
3
4
5
6
7
8
9
10
11
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
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
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
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
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
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
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
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
171
172
173
174
175
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
225
226
227
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
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
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
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
280
281 func compilerSupportsLocation() bool {
282 compiler, err := compilerVersion()
283 if err != nil {
284 return false
285 }
286 switch compiler.name {
287 case "gcc":
288
289
290
291
292
293 return compiler.major > 10
294 case "clang":
295
296
297
298
299 if inLUCIBuild() {
300 return false
301 }
302 return true
303 default:
304 return false
305 }
306 }
307
308
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
318
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
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
356
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
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
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
390
391
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
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
438
439 func (c *config) goCmd(subcommand string, args ...string) *exec.Cmd {
440 return c.goCmdWithExperiments(subcommand, args, nil)
441 }
442
443
444
445
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
457
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
487
488
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
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
518
519
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
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
547
548
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
566
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
587
588
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
605 func srcPath(path string) string {
606 return "./testdata/" + path
607 }
608
609
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
637
638
639
640
641
642
643
644 func hangProneCmd(name string, arg ...string) *exec.Cmd {
645 cmd := exec.Command(name, arg...)
646 makeHangProne(cmd)
647 return cmd
648 }
649
650
651
652 func makeHangProne(cmd *exec.Cmd) {
653 cmd.SysProcAttr = &syscall.SysProcAttr{
654 Pdeathsig: syscall.SIGKILL,
655 }
656 }
657
View as plain text