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