Skip to content

Commit 2931a42

Browse files
committed
fix(testrunner): suppress glibc nss dlopen reachable in --valgrind
1 parent 1ca866b commit 2931a42

2 files changed

Lines changed: 65 additions & 4 deletions

File tree

main.go

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,20 @@ func tinRuntimeDir() string {
180180
return filepath.Join(filepath.Dir(ex), "runtime")
181181
}
182182

183+
// valgrindNssSuppressionsPath returns the path to the glibc-nss
184+
// suppressions file shipped alongside the tin binary, or "" when it
185+
// can't be located. Used by --valgrind to silence the linker-level
186+
// reachable blocks from getaddrinfo's lazy dlopen of nss / resolv
187+
// modules.
188+
func valgrindNssSuppressionsPath() string {
189+
p := filepath.Join(tinRuntimeDir(), "valgrind-glibc-nss.supp")
190+
if _, err := os.Stat(p); err != nil {
191+
return ""
192+
}
193+
194+
return p
195+
}
196+
183197
// stdlibDirForDirectives returns the effective stdlib path for $TIN_STDLIB expansion.
184198
// Uses override if provided; otherwise falls back to <execDir>/stdlib.
185199
func stdlibDirForDirectives(override string) string {
@@ -2670,14 +2684,25 @@ func validateMemcheck(memcheck string) {
26702684
func memcheckCmd(memcheck, binary string, binArgs ...string) *exec.Cmd {
26712685
switch memcheck {
26722686
case "valgrind":
2673-
args := append([]string{
2687+
vgArgs := []string{
26742688
"--error-exitcode=1",
26752689
"--leak-check=full",
26762690
"--errors-for-leak-kinds=all",
2677-
binary,
2678-
}, binArgs...)
2691+
}
2692+
// Suppress glibc's lazy nss/resolver dlopen bookkeeping: the
2693+
// first getaddrinfo() in a process loads libnss_*/libresolv via
2694+
// __libc_dlopen_mode -> _dl_open and keeps those mappings live
2695+
// until exit, which valgrind reports as "still reachable". The
2696+
// suppression file at runtime/valgrind-glibc-nss.supp anchors
2697+
// each rule at `_dl_open` so real leaks elsewhere still surface.
2698+
if suppPath := valgrindNssSuppressionsPath(); suppPath != "" {
2699+
vgArgs = append(vgArgs, "--suppressions="+suppPath)
2700+
}
2701+
2702+
vgArgs = append(vgArgs, binary)
2703+
vgArgs = append(vgArgs, binArgs...)
26792704

2680-
return wrapExec("valgrind", args...)
2705+
return wrapExec("valgrind", vgArgs...)
26812706
case "leaks":
26822707
args := append([]string{"--atExit", "--", binary}, binArgs...)
26832708

runtime/valgrind-glibc-nss.supp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# runtime/valgrind-glibc-nss.supp -- valgrind suppressions for glibc's
2+
# lazy nss / resolver module loading.
3+
#
4+
# The first getaddrinfo() (and similar nss callers) in a glibc process
5+
# loads libnss_files.so / libnss_dns.so / libresolv.so via
6+
# __libc_dlopen_mode -> _dl_open and keeps the resulting mappings live
7+
# for the program's lifetime. The rtld bookkeeping for those .so's
8+
# (linkmap entries, version maps, scope arrays, cache strdup'd paths)
9+
# stays in glibc's globals with no dlclose() ever called -- valgrind
10+
# reports each as "still reachable".
11+
#
12+
# Verified by a pure-C reproducer that calls only getaddrinfo() +
13+
# freeaddrinfo() with zero Tin code on the stack: 9,816 bytes / 21
14+
# blocks / 8 records, byte-for-byte identical to what Tin's dns_test
15+
# shows. Every record's call chain terminates at `_dl_open` (the
16+
# dynamic linker's open-and-map entry), so we anchor the match there.
17+
# Real reachable leaks elsewhere in the program (no `_dl_open` frame
18+
# on the stack) still surface.
19+
20+
{
21+
glibc-rtld-dlopen-malloc-reachable
22+
Memcheck:Leak
23+
match-leak-kinds: reachable
24+
fun:malloc
25+
...
26+
fun:_dl_open
27+
}
28+
29+
{
30+
glibc-rtld-dlopen-calloc-reachable
31+
Memcheck:Leak
32+
match-leak-kinds: reachable
33+
fun:calloc
34+
...
35+
fun:_dl_open
36+
}

0 commit comments

Comments
 (0)