runtime: in asan mode call __lsan_do_leak_check when exiting

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>
This commit is contained in:
Ian Lance Taylor 2025-02-21 17:13:20 -08:00 committed by Gopher Robot
parent cad4dca518
commit 645ea53019
20 changed files with 394 additions and 7 deletions

View File

@ -2,5 +2,13 @@
### 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}

View File

@ -11,6 +11,7 @@ package cgotest
/*
#include <stdint.h>
#include <stdlib.h>
#include <dlfcn.h>
#cgo linux LDFLAGS: -ldl
@ -24,6 +25,7 @@ import "C"
import (
"testing"
"unsafe"
)
var callbacks int
@ -66,7 +68,9 @@ func loadThySelf(t *testing.T, symbol string) {
}
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

View File

@ -1098,6 +1098,7 @@ func testErrno(t *testing.T) {
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 {
@ -1106,7 +1107,6 @@ func testMultipleAssign(t *testing.T) {
} else if n != 0 || m != 234 {
t.Fatal("Strtol x2: ", n, m)
}
C.free(unsafe.Pointer(p))
}
var (
@ -1632,7 +1632,9 @@ func testNaming(t *testing.T) {
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)
}
}
@ -1881,6 +1883,7 @@ func test17537(t *testing.T) {
}
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)

View File

@ -328,6 +328,12 @@ func compilerRequiredAsanVersion(goos, goarch string) bool {
}
}
// 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
@ -377,7 +383,7 @@ func configure(sanitizer string) *config {
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")

View File

@ -0,0 +1,102 @@
// 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
}

View File

@ -0,0 +1,39 @@
// 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)
}

View File

@ -0,0 +1,42 @@
// 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)
}

View File

@ -0,0 +1,46 @@
// 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)
}

View File

@ -61,6 +61,11 @@ func asanpoison(addr unsafe.Pointer, sz uintptr)
//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
@ -68,3 +73,5 @@ func asanregisterglobals(addr unsafe.Pointer, n uintptr)
//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

View File

@ -13,6 +13,7 @@ package asan
#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)) {
@ -34,6 +35,14 @@ void __asan_poison_go(void *addr, uintptr_t 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

View File

@ -21,3 +21,5 @@ func asanwrite(addr unsafe.Pointer, sz uintptr) { throw("asan") }
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") }

View File

@ -69,6 +69,20 @@ TEXT runtime·asanregisterglobals(SB), NOSPLIT, $0-16
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)

View File

@ -58,6 +58,20 @@ TEXT runtime·asanregisterglobals(SB), NOSPLIT, $0-16
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

View File

@ -58,6 +58,20 @@ TEXT runtime·asanregisterglobals(SB), NOSPLIT, $0-16
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

View File

@ -58,6 +58,20 @@ TEXT runtime·asanregisterglobals(SB),NOSPLIT|NOFRAME,$0-16
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

View File

@ -52,6 +52,20 @@ TEXT runtime·asanregisterglobals(SB), NOSPLIT, $0-16
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

View File

@ -49,7 +49,15 @@ import "unsafe"
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
@ -143,7 +151,15 @@ func sysFault(v unsafe.Pointer, n uintptr) {
// 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

View File

@ -281,11 +281,27 @@ func main() {
}
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.
@ -302,7 +318,9 @@ func main() {
if panicking.Load() != 0 {
gopark(nil, nil, waitReasonPanicWait, traceBlockForever, 1)
}
runExitHooks(0)
if !exitHooksRun {
runExitHooks(0)
}
exit(0)
for {
@ -319,6 +337,11 @@ func os_beforeExit(exitCode int) {
if exitCode == 0 && raceenabled {
racefini()
}
// See comment in main, above.
if exitCode == 0 && asanenabled && (isarchive || islibrary || NumCgoCall() > 1) {
lsandoleakcheck()
}
}
func init() {

View File

@ -8,6 +8,7 @@ package runtime_test
import (
"bytes"
"internal/asan"
"internal/testenv"
"os"
"os/exec"
@ -20,6 +21,10 @@ import (
// 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" {

View File

@ -11,6 +11,7 @@ import (
"errors"
"flag"
"fmt"
"internal/asan"
"internal/platform"
"internal/syscall/unix"
"internal/testenv"
@ -334,6 +335,10 @@ func TestUnshareMountNameSpaceChroot(t *testing.T) {
// 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 {