1
2
3
4
5
6
7
8
9
10
11
12 package sanitizers_test
13
14 import (
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
34 var overcommit struct {
35 sync.Once
36 value int
37 err error
38 }
39
40
41 func 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
61 var env struct {
62 sync.Once
63 m map[string]string
64 err error
65 }
66
67
68 func 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
91 func 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
99 func 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
114 func 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
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
140 func 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
152
153
154
155
156
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
194 type version struct {
195 name string
196 major, minor int
197 }
198
199 var compiler struct {
200 sync.Once
201 version
202 err error
203 }
204
205
206
207
208
209 func 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
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
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
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
261
262 func 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
272
273
274
275 if inLUCIBuild() {
276 return false
277 }
278 return true
279 default:
280 return false
281 }
282 }
283
284
285 func 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
294
295 func 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
307 func 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
331 type compilerCheck struct {
332 once sync.Once
333 err error
334 skip bool
335 }
336
337 type config struct {
338 sanitizer string
339
340 cFlags, ldFlags, goFlags []string
341
342 sanitizerCheck, runtimeCheck compilerCheck
343 }
344
345 var configs struct {
346 sync.Mutex
347 m map[string]*config
348 }
349
350
351 func 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
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
400
401 func (c *config) goCmd(subcommand string, args ...string) *exec.Cmd {
402 return c.goCmdWithExperiments(subcommand, args, nil)
403 }
404
405
406
407
408 func (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
419
420 func (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
434 var cMain = []byte(`
435 int main() {
436 return 0;
437 }
438 `)
439
440 var cLibFuzzerInput = []byte(`
441 #include <stddef.h>
442 int LLVMFuzzerTestOneInput(char *data, size_t size) {
443 return 0;
444 }
445 `)
446
447 func (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
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
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
498
499 func (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
513 func (c *config) checkRuntime() (skip bool, err error) {
514 if c.sanitizer != "thread" {
515 return false, nil
516 }
517
518
519
520
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
538 func srcPath(path string) string {
539 return filepath.Join("testdata", path)
540 }
541
542
543 type tempDir struct {
544 base string
545 }
546
547 func (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
557 func (d *tempDir) Base() string {
558 return d.base
559 }
560
561 func (d *tempDir) Join(name string) string {
562 return filepath.Join(d.base, name)
563 }
564
565 func 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
575
576
577
578
579
580
581
582 func 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
View as plain text