1
2
3
4
5
6
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 "unicode"
24 )
25
26 var overcommit struct {
27 sync.Once
28 value int
29 err error
30 }
31
32
33 func 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
53 var env struct {
54 sync.Once
55 m map[string]string
56 err error
57 }
58
59
60 func 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
83 func 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
91 func 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
100 func 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
112
113
114
115
116
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
154 type version struct {
155 name string
156 major, minor int
157 }
158
159 var compiler struct {
160 sync.Once
161 version
162 err error
163 }
164
165
166
167
168
169 func 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
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
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
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
221
222
223 func compilerSupportsLocation() bool {
224 compiler, err := compilerVersion()
225 if err != nil {
226 return false
227 }
228 switch compiler.name {
229 case "gcc":
230 return compiler.major >= 10
231 case "clang":
232 return true
233 default:
234 return false
235 }
236 }
237
238 type compilerCheck struct {
239 once sync.Once
240 err error
241 skip bool
242 }
243
244 type config struct {
245 sanitizer string
246
247 cFlags, ldFlags, goFlags []string
248
249 sanitizerCheck, runtimeCheck compilerCheck
250 }
251
252 var configs struct {
253 sync.Mutex
254 m map[string]*config
255 }
256
257
258 func configure(sanitizer string) *config {
259 configs.Lock()
260 defer configs.Unlock()
261 if c, ok := configs.m[sanitizer]; ok {
262 return c
263 }
264
265 c := &config{
266 sanitizer: sanitizer,
267 cFlags: []string{"-fsanitize=" + sanitizer},
268 ldFlags: []string{"-fsanitize=" + sanitizer},
269 }
270
271 if testing.Verbose() {
272 c.goFlags = append(c.goFlags, "-x")
273 }
274
275 switch sanitizer {
276 case "memory":
277 c.goFlags = append(c.goFlags, "-msan")
278
279 case "thread":
280 c.goFlags = append(c.goFlags, "--installsuffix=tsan")
281 compiler, _ := compilerVersion()
282 if compiler.name == "gcc" {
283 c.cFlags = append(c.cFlags, "-fPIC")
284 c.ldFlags = append(c.ldFlags, "-fPIC", "-static-libtsan")
285 }
286
287 case "address":
288 c.goFlags = append(c.goFlags, "-asan")
289
290 c.cFlags = append(c.cFlags, "-g")
291
292 default:
293 panic(fmt.Sprintf("unrecognized sanitizer: %q", sanitizer))
294 }
295
296 if configs.m == nil {
297 configs.m = make(map[string]*config)
298 }
299 configs.m[sanitizer] = c
300 return c
301 }
302
303
304
305 func (c *config) goCmd(subcommand string, args ...string) *exec.Cmd {
306 cmd := exec.Command("go", subcommand)
307 cmd.Args = append(cmd.Args, c.goFlags...)
308 cmd.Args = append(cmd.Args, args...)
309 replaceEnv(cmd, "CGO_CFLAGS", strings.Join(c.cFlags, " "))
310 replaceEnv(cmd, "CGO_LDFLAGS", strings.Join(c.ldFlags, " "))
311 return cmd
312 }
313
314
315
316 func (c *config) skipIfCSanitizerBroken(t *testing.T) {
317 check := &c.sanitizerCheck
318 check.once.Do(func() {
319 check.skip, check.err = c.checkCSanitizer()
320 })
321 if check.err != nil {
322 t.Helper()
323 if check.skip {
324 t.Skip(check.err)
325 }
326 t.Fatal(check.err)
327 }
328 }
329
330 var cMain = []byte(`
331 int main() {
332 return 0;
333 }
334 `)
335
336 func (c *config) checkCSanitizer() (skip bool, err error) {
337 dir, err := os.MkdirTemp("", c.sanitizer)
338 if err != nil {
339 return false, fmt.Errorf("failed to create temp directory: %v", err)
340 }
341 defer os.RemoveAll(dir)
342
343 src := filepath.Join(dir, "return0.c")
344 if err := os.WriteFile(src, cMain, 0600); err != nil {
345 return false, fmt.Errorf("failed to write C source file: %v", err)
346 }
347
348 dst := filepath.Join(dir, "return0")
349 cmd, err := cc(c.cFlags...)
350 if err != nil {
351 return false, err
352 }
353 cmd.Args = append(cmd.Args, c.ldFlags...)
354 cmd.Args = append(cmd.Args, "-o", dst, src)
355 out, err := cmd.CombinedOutput()
356 if err != nil {
357 if bytes.Contains(out, []byte("-fsanitize")) &&
358 (bytes.Contains(out, []byte("unrecognized")) ||
359 bytes.Contains(out, []byte("unsupported"))) {
360 return true, errors.New(string(out))
361 }
362 return true, fmt.Errorf("%#q failed: %v\n%s", strings.Join(cmd.Args, " "), err, out)
363 }
364
365 if out, err := exec.Command(dst).CombinedOutput(); err != nil {
366 if os.IsNotExist(err) {
367 return true, fmt.Errorf("%#q failed to produce executable: %v", strings.Join(cmd.Args, " "), err)
368 }
369 snippet, _, _ := bytes.Cut(out, []byte("\n"))
370 return true, fmt.Errorf("%#q generated broken executable: %v\n%s", strings.Join(cmd.Args, " "), err, snippet)
371 }
372
373 return false, nil
374 }
375
376
377
378 func (c *config) skipIfRuntimeIncompatible(t *testing.T) {
379 check := &c.runtimeCheck
380 check.once.Do(func() {
381 check.skip, check.err = c.checkRuntime()
382 })
383 if check.err != nil {
384 t.Helper()
385 if check.skip {
386 t.Skip(check.err)
387 }
388 t.Fatal(check.err)
389 }
390 }
391
392 func (c *config) checkRuntime() (skip bool, err error) {
393 if c.sanitizer != "thread" {
394 return false, nil
395 }
396
397
398
399
400 cmd, err := cc(c.cFlags...)
401 if err != nil {
402 return false, err
403 }
404 cmd.Args = append(cmd.Args, "-dM", "-E", "../../../src/runtime/cgo/libcgo.h")
405 cmdStr := strings.Join(cmd.Args, " ")
406 out, err := cmd.CombinedOutput()
407 if err != nil {
408 return false, fmt.Errorf("%#q exited with %v\n%s", cmdStr, err, out)
409 }
410 if !bytes.Contains(out, []byte("#define CGO_TSAN")) {
411 return true, fmt.Errorf("%#q did not define CGO_TSAN", cmdStr)
412 }
413 return false, nil
414 }
415
416
417 func srcPath(path string) string {
418 return filepath.Join("testdata", path)
419 }
420
421
422 type tempDir struct {
423 base string
424 }
425
426 func (d *tempDir) RemoveAll(t *testing.T) {
427 t.Helper()
428 if d.base == "" {
429 return
430 }
431 if err := os.RemoveAll(d.base); err != nil {
432 t.Fatalf("Failed to remove temp dir: %v", err)
433 }
434 }
435
436 func (d *tempDir) Join(name string) string {
437 return filepath.Join(d.base, name)
438 }
439
440 func newTempDir(t *testing.T) *tempDir {
441 t.Helper()
442 dir, err := os.MkdirTemp("", filepath.Dir(t.Name()))
443 if err != nil {
444 t.Fatalf("Failed to create temp dir: %v", err)
445 }
446 return &tempDir{base: dir}
447 }
448
449
450
451
452
453
454
455
456
457 func hangProneCmd(name string, arg ...string) *exec.Cmd {
458 cmd := exec.Command(name, arg...)
459 cmd.SysProcAttr = &syscall.SysProcAttr{
460 Pdeathsig: syscall.SIGKILL,
461 }
462 return cmd
463 }
464
465
466
467 func mSanSupported(goos, goarch string) bool {
468 switch goos {
469 case "linux":
470 return goarch == "amd64" || goarch == "arm64"
471 default:
472 return false
473 }
474 }
475
476
477
478 func aSanSupported(goos, goarch string) bool {
479 switch goos {
480 case "linux":
481 return goarch == "amd64" || goarch == "arm64"
482 default:
483 return false
484 }
485 }
486
View as plain text