This enables the ASAN default behavior of reporting C memory leaks.
It can be disabled with ASAN_OPTIONS=detect_leaks=0.
Fixes #67833
Change-Id: I420da1b5d79cf70d8cf134eaf97bf0a22f61ffd0
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-asan-clang15,gotip-linux-arm64-asan-clang15
Reviewed-on: https://go-review.googlesource.com/c/go/+/651755
Reviewed-by: Cherry Mui <cherryyz@google.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
Auto-Submit: Ian Lance Taylor <iant@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
### Go command {#go-command}
+The `go build` `-asan` option now defaults to doing leak detection at
+program exit.
+This will report an error if memory allocated by C is not freed and is
+not referenced by any other memory allocated by either C or Go.
+These new error reports may be disabled by setting
+`ASAN_OPTIONS=detect_leaks=0` in the environment when running the
+program.
+
### Cgo {#cgo}
/*
#include <stdint.h>
+#include <stdlib.h>
#include <dlfcn.h>
#cgo linux LDFLAGS: -ldl
import (
"testing"
+ "unsafe"
)
var callbacks int
}
defer C.dlclose4029(this_process)
- symbol_address := C.dlsym4029(this_process, C.CString(symbol))
+ symCStr := C.CString(symbol)
+ defer C.free(unsafe.Pointer(symCStr))
+ symbol_address := C.dlsym4029(this_process, symCStr)
if symbol_address == 0 {
t.Error("dlsym:", C.GoString(C.dlerror()))
return
func testMultipleAssign(t *testing.T) {
p := C.CString("234")
n, m := C.strtol(p, nil, 345), C.strtol(p, nil, 10)
+ defer C.free(unsafe.Pointer(p))
if runtime.GOOS == "openbsd" {
// Bug in OpenBSD strtol(3) - base > 36 succeeds.
if (n != 0 && n != 239089) || m != 234 {
} else if n != 0 || m != 234 {
t.Fatal("Strtol x2: ", n, m)
}
- C.free(unsafe.Pointer(p))
}
var (
func test6907(t *testing.T) {
want := "yarn"
- if got := C.GoString(C.Issue6907CopyString(want)); got != want {
+ s := C.Issue6907CopyString(want)
+ defer C.free(unsafe.Pointer(s))
+ if got := C.GoString(s); got != want {
t.Errorf("C.GoString(C.Issue6907CopyString(%q)) == %q, want %q", want, got, want)
}
}
}
p := (*C.char)(C.malloc(1))
+ defer C.free(unsafe.Pointer(p))
*p = 17
if got, want := C.F17537(&p), C.int(17); got != want {
t.Errorf("got %d, want %d", got, want)
}
}
+// compilerRequiredLsanVersion reports whether the compiler is the
+// version required by Lsan.
+func compilerRequiredLsanVersion(goos, goarch string) bool {
+ return compilerRequiredAsanVersion(goos, goarch)
+}
+
type compilerCheck struct {
once sync.Once
err error
c.ldFlags = append(c.ldFlags, "-fPIC", "-static-libtsan")
}
- case "address":
+ case "address", "leak":
c.goFlags = append(c.goFlags, "-asan")
// Set the debug mode to print the C stack trace.
c.cFlags = append(c.cFlags, "-g")
--- /dev/null
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build linux || (freebsd && amd64)
+
+package sanitizers_test
+
+import (
+ "internal/platform"
+ "internal/testenv"
+ "strings"
+ "testing"
+)
+
+func TestLSAN(t *testing.T) {
+ config := mustHaveLSAN(t)
+
+ t.Parallel()
+ mustRun(t, config.goCmd("build", "std"))
+
+ cases := []struct {
+ src string
+ leakError string
+ errorLocation string
+ }{
+ {src: "lsan1.go", leakError: "detected memory leaks", errorLocation: "lsan1.go:11"},
+ {src: "lsan2.go"},
+ {src: "lsan3.go"},
+ }
+ for _, tc := range cases {
+ name := strings.TrimSuffix(tc.src, ".go")
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+
+ dir := newTempDir(t)
+ defer dir.RemoveAll(t)
+
+ outPath := dir.Join(name)
+ mustRun(t, config.goCmd("build", "-o", outPath, srcPath(tc.src)))
+
+ cmd := hangProneCmd(outPath)
+ if tc.leakError == "" {
+ mustRun(t, cmd)
+ } else {
+ outb, err := cmd.CombinedOutput()
+ out := string(outb)
+ if err != nil || len(out) > 0 {
+ t.Logf("%s\n%v\n%s", cmd, err, out)
+ }
+ if err != nil && strings.Contains(out, tc.leakError) {
+ // This string is output if the
+ // sanitizer library needs a
+ // symbolizer program and can't find it.
+ const noSymbolizer = "external symbolizer"
+ if tc.errorLocation != "" &&
+ !strings.Contains(out, tc.errorLocation) &&
+ !strings.Contains(out, noSymbolizer) &&
+ compilerSupportsLocation() {
+
+ t.Errorf("output does not contain expected location of the error %q", tc.errorLocation)
+ }
+ } else {
+ t.Errorf("output does not contain expected leak error %q", tc.leakError)
+ }
+
+ // Make sure we can disable the leak check.
+ cmd = hangProneCmd(outPath)
+ replaceEnv(cmd, "ASAN_OPTIONS", "detect_leaks=0")
+ mustRun(t, cmd)
+ }
+ })
+ }
+}
+
+func mustHaveLSAN(t *testing.T) *config {
+ testenv.MustHaveGoBuild(t)
+ testenv.MustHaveCGO(t)
+ goos, err := goEnv("GOOS")
+ if err != nil {
+ t.Fatal(err)
+ }
+ goarch, err := goEnv("GOARCH")
+ if err != nil {
+ t.Fatal(err)
+ }
+ // LSAN is a subset of ASAN, so just check for ASAN support.
+ if !platform.ASanSupported(goos, goarch) {
+ t.Skipf("skipping on %s/%s; -asan option is not supported.", goos, goarch)
+ }
+
+ if !compilerRequiredLsanVersion(goos, goarch) {
+ t.Skipf("skipping on %s/%s: too old version of compiler", goos, goarch)
+ }
+
+ requireOvercommit(t)
+
+ config := configure("leak")
+ config.skipIfCSanitizerBroken(t)
+
+ return config
+}
--- /dev/null
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+/*
+#include <stdlib.h>
+
+int* test() {
+ return malloc(sizeof(int));
+}
+
+void clearStack(int n) {
+ if (n > 0) {
+ clearStack(n - 1);
+ }
+}
+
+*/
+import "C"
+
+//go:noinline
+func F() {
+ C.test()
+}
+
+func clearStack(n int) {
+ if n > 0 {
+ clearStack(n - 1)
+ }
+}
+
+func main() {
+ // Test should fail: memory allocated by C is leaked.
+ F()
+ clearStack(100)
+ C.clearStack(100)
+}
--- /dev/null
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+/*
+#include <stdlib.h>
+
+int* test() {
+ return malloc(sizeof(int));
+}
+
+void clearStack(int n) {
+ if (n > 0) {
+ clearStack(n - 1);
+ }
+}
+
+*/
+import "C"
+
+var p *C.int
+
+//go:noinline
+func F() {
+ p = C.test()
+}
+
+func clearStack(n int) {
+ if n > 0 {
+ clearStack(n - 1)
+ }
+}
+
+func main() {
+ // Test should pass: memory allocated by C does not leak
+ // because a Go global variable points to it.
+ F()
+ clearStack(100)
+ C.clearStack(100)
+}
--- /dev/null
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+/*
+#include <stdlib.h>
+
+int* test() {
+ return malloc(sizeof(int));
+}
+
+void clearStack(int n) {
+ if (n > 0) {
+ clearStack(n - 1);
+ }
+}
+
+*/
+import "C"
+
+type S struct {
+ p *C.int
+}
+
+var p *S
+
+//go:noinline
+func F() {
+ p = &S{p: C.test()}
+}
+
+func clearStack(n int) {
+ if n > 0 {
+ clearStack(n - 1)
+ }
+}
+
+func main() {
+ // Test should pass: memory allocated by C does not leak
+ // because a Go global variable points to it.
+ F()
+ clearStack(100)
+ C.clearStack(100)
+}
//go:noescape
func asanregisterglobals(addr unsafe.Pointer, n uintptr)
+//go:noescape
+func lsanregisterrootregion(addr unsafe.Pointer, n uintptr)
+
+func lsandoleakcheck()
+
// These are called from asan_GOARCH.s
//
//go:cgo_import_static __asan_read_go
//go:cgo_import_static __asan_unpoison_go
//go:cgo_import_static __asan_poison_go
//go:cgo_import_static __asan_register_globals_go
+//go:cgo_import_static __lsan_register_root_region_go
+//go:cgo_import_static __lsan_do_leak_check_go
#include <stdbool.h>
#include <stdint.h>
#include <sanitizer/asan_interface.h>
+#include <sanitizer/lsan_interface.h>
void __asan_read_go(void *addr, uintptr_t sz, void *sp, void *pc) {
if (__asan_region_is_poisoned(addr, sz)) {
__asan_poison_memory_region(addr, sz);
}
+void __lsan_register_root_region_go(void *addr, uintptr_t sz) {
+ __lsan_register_root_region(addr, sz);
+}
+
+void __lsan_do_leak_check_go(void) {
+ __lsan_do_leak_check();
+}
+
// Keep in sync with the definition in compiler-rt
// https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/asan/asan_interface_internal.h#L41
// This structure is used to describe the source location of
func asanunpoison(addr unsafe.Pointer, sz uintptr) { throw("asan") }
func asanpoison(addr unsafe.Pointer, sz uintptr) { throw("asan") }
func asanregisterglobals(addr unsafe.Pointer, sz uintptr) { throw("asan") }
+func lsanregisterrootregion(unsafe.Pointer, uintptr) { throw("asan") }
+func lsandoleakcheck() { throw("asan") }
MOVQ $__asan_register_globals_go(SB), AX
JMP asancall<>(SB)
+// func runtime·lsanregisterrootregion(addr unsafe.Pointer, n uintptr)
+TEXT runtime·lsanregisterrootregion(SB), NOSPLIT, $0-16
+ MOVQ addr+0(FP), RARG0
+ MOVQ n+8(FP), RARG1
+ // void __lsan_register_root_region_go(void *addr, uintptr_t sz)
+ MOVQ $__lsan_register_root_region_go(SB), AX
+ JMP asancall<>(SB)
+
+// func runtime·lsandoleakcheck()
+TEXT runtime·lsandoleakcheck(SB), NOSPLIT, $0-0
+ // void __lsan_do_leak_check_go(void);
+ MOVQ $__lsan_do_leak_check_go(SB), AX
+ JMP asancall<>(SB)
+
// Switches SP to g0 stack and calls (AX). Arguments already set.
TEXT asancall<>(SB), NOSPLIT, $0-0
get_tls(R12)
MOVD $__asan_register_globals_go(SB), FARG
JMP asancall<>(SB)
+// func runtime·lsanregisterrootregion(addr unsafe.Pointer, n uintptr)
+TEXT runtime·lsanregisterrootregion(SB), NOSPLIT, $0-16
+ MOVD addr+0(FP), RARG0
+ MOVD n+8(FP), RARG1
+ // void __lsan_register_root_region_go(void *addr, uintptr_t n);
+ MOVD $__lsan_register_root_region_go(SB), FARG
+ JMP asancall<>(SB)
+
+// func runtime·lsandoleakcheck()
+TEXT runtime·lsandoleakcheck(SB), NOSPLIT, $0-0
+ // void __lsan_do_leak_check_go(void);
+ MOVD $__lsan_do_leak_check_go(SB), FARG
+ JMP asancall<>(SB)
+
// Switches SP to g0 stack and calls (FARG). Arguments already set.
TEXT asancall<>(SB), NOSPLIT, $0-0
MOVD RSP, R19 // callee-saved
MOVV $__asan_register_globals_go(SB), FARG
JMP asancall<>(SB)
+// func runtime·lsanregisterrootregion(addr unsafe.Pointer, n uintptr)
+TEXT runtime·lsanregisterrootregion(SB), NOSPLIT, $0-16
+ MOVV addr+0(FP), RARG0
+ MOVV n+8(FP), RARG1
+ // void __lsan_register_root_region_go(void *addr, uintptr_t n);
+ MOVV $__lsan_register_root_region_go(SB), FARG
+ JMP asancall<>(SB)
+
+// func runtime·lsandoleakcheck()
+TEXT runtime·lsandoleakcheck(SB), NOSPLIT, $0-0
+ // void __lsan_do_leak_check_go(void);
+ MOVV $__lsan_do_leak_check_go(SB), FARG
+ JMP asancall<>(SB)
+
// Switches SP to g0 stack and calls (FARG). Arguments already set.
TEXT asancall<>(SB), NOSPLIT, $0-0
MOVV R3, R23 // callee-saved
MOVD $__asan_register_globals_go(SB), FARG
BR asancall<>(SB)
+// func runtime·lsanregisterrootregion(addr unsafe.Pointer, n uintptr)
+TEXT runtime·lsanregisterrootregion(SB),NOSPLIT|NOFRAME,$0-16
+ MOVD addr+0(FP), RARG0
+ MOVD n+8(FP), RARG1
+ // void __lsan_register_root_region_go(void *addr, uintptr_t n);
+ MOVD $__lsan_register_root_region_go(SB), FARG
+ BR asancall<>(SB)
+
+// func runtime·lsandoleakcheck()
+TEXT runtime·lsandoleakcheck(SB), NOSPLIT|NOFRAME, $0-0
+ // void __lsan_do_leak_check_go(void);
+ MOVD $__lsan_do_leak_check_go(SB), FARG
+ BR asancall<>(SB)
+
// Switches SP to g0 stack and calls (FARG). Arguments already set.
TEXT asancall<>(SB), NOSPLIT, $0-0
// LR saved in generated prologue
MOV $__asan_register_globals_go(SB), X14
JMP asancall<>(SB)
+// func runtime·lsanregisterrootregion(addr unsafe.Pointer, n uintptr)
+TEXT runtime·lsanregisterrootregion(SB), NOSPLIT, $0-16
+ MOV addr+0(FP), X10
+ MOV n+8(FP), X11
+ // void __lsan_register_root_region_go(void *addr, uintptr_t n);
+ MOV $__lsan_register_root_region_go(SB), X14
+ JMP asancall<>(SB)
+
+// func runtime·lsandoleakcheck()
+TEXT runtime·lsandoleakcheck(SB), NOSPLIT, $0-0
+ // void __lsan_do_leak_check_go(void);
+ MOV $__lsan_do_leak_check_go(SB), X14
+ JMP asancall<>(SB)
+
// Switches SP to g0 stack and calls (X14). Arguments already set.
TEXT asancall<>(SB), NOSPLIT, $0-0
MOV X2, X8 // callee-saved
func sysAlloc(n uintptr, sysStat *sysMemStat, vmaName string) unsafe.Pointer {
sysStat.add(int64(n))
gcController.mappedReady.Add(int64(n))
- return sysAllocOS(n, vmaName)
+ p := sysAllocOS(n, vmaName)
+
+ // When using ASAN leak detection, we must tell ASAN about
+ // cases where we store pointers in mmapped memory.
+ if asanenabled {
+ lsanregisterrootregion(p, n)
+ }
+
+ return p
}
// sysUnused transitions a memory region from Ready to Prepared. It notifies the
// may use larger alignment, so the caller must be careful to realign the
// memory obtained by sysReserve.
func sysReserve(v unsafe.Pointer, n uintptr, vmaName string) unsafe.Pointer {
- return sysReserveOS(v, n, vmaName)
+ p := sysReserveOS(v, n, vmaName)
+
+ // When using ASAN leak detection, we must tell ASAN about
+ // cases where we store pointers in mmapped memory.
+ if asanenabled {
+ lsanregisterrootregion(p, n)
+ }
+
+ return p
}
// sysMap transitions a memory region from Reserved to Prepared. It ensures the
}
fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn()
+
+ exitHooksRun := false
if raceenabled {
runExitHooks(0) // run hooks now, since racefini does not return
+ exitHooksRun = true
racefini()
}
+ // Check for C memory leaks if using ASAN and we've made cgo calls,
+ // or if we are running as a library in a C program.
+ // We always make one cgo call, above, to notify_runtime_init_done,
+ // so we ignore that one.
+ // No point in leak checking if no cgo calls, since leak checking
+ // just looks for objects allocated using malloc and friends.
+ // Just checking iscgo doesn't help because asan implies iscgo.
+ if asanenabled && (isarchive || islibrary || NumCgoCall() > 1) {
+ runExitHooks(0) // lsandoleakcheck may not return
+ exitHooksRun = true
+ lsandoleakcheck()
+ }
+
// Make racy client program work: if panicking on
// another goroutine at the same time as main returns,
// let the other goroutine finish printing the panic trace.
if panicking.Load() != 0 {
gopark(nil, nil, waitReasonPanicWait, traceBlockForever, 1)
}
- runExitHooks(0)
+ if !exitHooksRun {
+ runExitHooks(0)
+ }
exit(0)
for {
if exitCode == 0 && raceenabled {
racefini()
}
+
+ // See comment in main, above.
+ if exitCode == 0 && asanenabled && (isarchive || islibrary || NumCgoCall() > 1) {
+ lsandoleakcheck()
+ }
}
func init() {
import (
"bytes"
+ "internal/asan"
"internal/testenv"
"os"
"os/exec"
// TestUsingVDSO tests that we are actually using the VDSO to fetch
// the time.
func TestUsingVDSO(t *testing.T) {
+ if asan.Enabled {
+ t.Skip("test fails with ASAN beause the ASAN leak checker won't run under strace")
+ }
+
const calls = 100
if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
"errors"
"flag"
"fmt"
+ "internal/asan"
"internal/platform"
"internal/syscall/unix"
"internal/testenv"
// Test for Issue 29789: unshare fails when uid/gid mapping is specified
func TestUnshareUidGidMapping(t *testing.T) {
+ if asan.Enabled {
+ t.Skip("test fails with ASAN beause the ASAN leak checker fails finding memory regions")
+ }
+
if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
defer os.Exit(0)
if err := syscall.Chroot(os.TempDir()); err != nil {