cmd/cgo/internal/testsanitizers: bound ASAN C support probe

The C sanitizer support check builds and runs a tiny C program before
the Go ASAN tests run. Some libasan versions run a slow LeakSanitizer
check at process exit even for this no-op program, which can cause
TestASAN to time out before it starts testing Go binaries.

For ASAN support probes, set leak_check_at_exit=0. This avoids the
implicit exit-time leak scan for the probe without disabling explicit
LSAN tests. Also run the compile and execute probes with a fixed local
timeout so broken C sanitizer configurations are skipped instead of
hitting the package-level test timeout.

Fixes #72996

Change-Id: I53a0269324f8f0eb214df81aeeecc72dfc432fe1
Reviewed-on: https://go-review.googlesource.com/c/go/+/774660
Reviewed-by: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
This commit is contained in:
qmuntal 2026-05-06 12:49:58 +02:00 committed by Quim Muntal
parent 834214f787
commit e30b75a910
3 changed files with 65 additions and 10 deletions

View file

@ -13,6 +13,7 @@ package sanitizers_test
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
@ -110,6 +111,24 @@ func appendExperimentEnv(cmd *exec.Cmd, experiments []string) {
cmd.Env = append(cmd.Env, "GOEXPERIMENT="+exps)
}
func appendASANOptions(cmd *exec.Cmd, opts ...string) {
if cmd.Env == nil {
cmd.Env = cmd.Environ()
}
var asanOptions string
for _, evar := range cmd.Env {
name, value, ok := strings.Cut(evar, "=")
if ok && name == "ASAN_OPTIONS" {
asanOptions = value
}
}
if asanOptions != "" {
asanOptions += ":"
}
asanOptions += strings.Join(opts, ":")
cmd.Env = append(cmd.Env, "ASAN_OPTIONS="+asanOptions)
}
// mustRun executes t and fails cmd with a well-formatted message if it fails.
func mustRun(t *testing.T, cmd *exec.Cmd) {
t.Helper()
@ -137,7 +156,7 @@ func mustRun(t *testing.T, cmd *exec.Cmd) {
}
// cc returns a cmd that executes `$(go env CC) $(go env GOGCCFLAGS) $args`.
func cc(args ...string) (*exec.Cmd, error) {
func cc(ctx context.Context, args ...string) (*exec.Cmd, error) {
CC, err := goEnv("CC")
if err != nil {
return nil, err
@ -186,7 +205,7 @@ func cc(args ...string) (*exec.Cmd, error) {
flags = append(flags, GOGCCFLAGS[start:])
}
cmd := exec.Command(CC, flags...)
cmd := exec.CommandContext(ctx, CC, flags...)
cmd.Args = append(cmd.Args, args...)
return cmd, nil
}
@ -211,7 +230,7 @@ func compilerVersion() (version, error) {
compiler.err = func() error {
compiler.name = "unknown"
cmd, err := cc("--version")
cmd, err := cc(context.Background(), "--version")
if err != nil {
return err
}
@ -224,7 +243,7 @@ func compilerVersion() (version, error) {
var match [][]byte
if bytes.HasPrefix(out, []byte("gcc")) {
compiler.name = "gcc"
cmd, err := cc("-dumpfullversion", "-dumpversion")
cmd, err := cc(context.Background(), "-dumpfullversion", "-dumpversion")
if err != nil {
return err
}
@ -464,6 +483,11 @@ int LLVMFuzzerTestOneInput(char *data, size_t size) {
`)
func (c *config) checkCSanitizer() (skip bool, err error) {
// The sanitizer probes compile and run tiny C programs. If either step
// takes longer than this, treat the C sanitizer configuration as broken
// instead of letting the package-level test timeout fire.
probeTimeout := 20 * time.Second
dir, err := os.MkdirTemp("", c.sanitizer)
if err != nil {
return false, fmt.Errorf("failed to create temp directory: %v", err)
@ -481,14 +505,26 @@ func (c *config) checkCSanitizer() (skip bool, err error) {
}
dst := filepath.Join(dir, "return0")
cmd, err := cc(c.cFlags...)
compileCtx, cancelCompile := context.WithTimeout(context.Background(), probeTimeout)
defer cancelCompile()
cmd, err := cc(compileCtx, c.cFlags...)
if err != nil {
return false, err
}
cmd.Args = append(cmd.Args, c.ldFlags...)
cmd.Args = append(cmd.Args, "-o", dst, src)
if c.sanitizer == "address" {
// This is only a compiler support probe for ASAN. Some libasan versions
// run a slow LeakSanitizer check at exit even for this no-op C program,
// which can hang TestASAN before it starts testing Go binaries.
appendASANOptions(cmd, "leak_check_at_exit=0")
}
makeHangProne(cmd)
out, err := cmd.CombinedOutput()
if err != nil {
if errors.Is(compileCtx.Err(), context.DeadlineExceeded) {
return true, fmt.Errorf("%#q timed out after %v", cmd, probeTimeout)
}
if bytes.Contains(out, []byte("-fsanitize")) &&
(bytes.Contains(out, []byte("unrecognized")) ||
bytes.Contains(out, []byte("unsupported"))) {
@ -502,7 +538,20 @@ func (c *config) checkCSanitizer() (skip bool, err error) {
return false, nil
}
if out, err := exec.Command(dst).CombinedOutput(); err != nil {
runCtx, cancelRun := context.WithTimeout(context.Background(), probeTimeout)
defer cancelRun()
cmd = exec.CommandContext(runCtx, dst)
makeHangProne(cmd)
if c.sanitizer == "address" {
// Match the compile-time probe above: avoid libasan's implicit LSan exit
// scan for this standalone C binary. The explicit LSAN tests still run
// with leak checking enabled.
appendASANOptions(cmd, "leak_check_at_exit=0")
}
if out, err := cmd.CombinedOutput(); err != nil {
if errors.Is(runCtx.Err(), context.DeadlineExceeded) {
return true, fmt.Errorf("%#q timed out after %v", cmd, probeTimeout)
}
if os.IsNotExist(err) {
return true, fmt.Errorf("%#q failed to produce executable: %v", cmd, err)
}
@ -537,7 +586,7 @@ func (c *config) checkRuntime() (skip bool, err error) {
// libcgo.h sets CGO_TSAN if it detects TSAN support in the C compiler.
// Dump the preprocessor defines to check that works.
// (Sometimes it doesn't: see https://golang.org/issue/15983.)
cmd, err := cc(c.cFlags...)
cmd, err := cc(context.Background(), c.cFlags...)
if err != nil {
return false, err
}
@ -594,8 +643,14 @@ func newTempDir(t *testing.T) *tempDir {
// leak.
func hangProneCmd(name string, arg ...string) *exec.Cmd {
cmd := exec.Command(name, arg...)
makeHangProne(cmd)
return cmd
}
// makeHangProne configures cmd to receive SIGKILL when the parent process receives SIGINT.
// See [hangProneCmd] for details.
func makeHangProne(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
Pdeathsig: syscall.SIGKILL,
}
return cmd
}

View file

@ -82,7 +82,7 @@ func TestShared(t *testing.T) {
}
dstBin := dir.Join(name)
cmd, err := cc(config.cFlags...)
cmd, err := cc(t.Context(), config.cFlags...)
if err != nil {
t.Fatal(err)
}

View file

@ -56,7 +56,7 @@ func TestLibFuzzer(t *testing.T) {
mustRun(t, config.goCmd("build", "-buildmode=c-archive", "-o", archivePath, srcPath(tc.goSrc)))
// build C code (if any) and link with Go code
cmd, err := cc(config.cFlags...)
cmd, err := cc(t.Context(), config.cFlags...)
if err != nil {
t.Fatalf("error running cc: %v", err)
}