Skip to content

Commit 731dc7d

Browse files
committed
test(callgrind): add C reproducer for runtime obj-skip cxt==0 leak
Minimal C test that triggers the leak path where a function in a skipped object becomes a top-level fn= block in the dump. The trigger: the lib calls CALLGRIND_START_INSTRUMENTATION from inside one of its own functions, so the first BB callgrind sees post-start lives in the skipped object. With cxt == 0 at that point, setup_bbcc force-pushes the skipped fn as the new top context and it leaks into the output. Currently RED: post-check fails because fn=skipme_run appears in the output despite skipme_run's containing .so being on the skip list.
1 parent 48472d5 commit 731dc7d

6 files changed

Lines changed: 79 additions & 3 deletions

File tree

callgrind/tests/Makefile.am

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ EXTRA_DIST = \
1313
find_debuginfo.vgtest find_debuginfo.stderr.exp find_debuginfo.post.exp \
1414
runtime_obj_skip_py.vgtest runtime_obj_skip_py.stderr.exp runtime_obj_skip_py.post.exp \
1515
runtime_obj_skip_py.py runtime_obj_skip_py_shim.c \
16+
runtime_obj_skip_c.vgtest runtime_obj_skip_c.stderr.exp runtime_obj_skip_c.post.exp \
17+
runtime_obj_skip_c.c runtime_obj_skip_c_lib.c \
1618
bug497723.stderr.exp bug497723.post.exp bug497723.vgtest \
1719
simwork1.vgtest simwork1.stdout.exp simwork1.stderr.exp \
1820
simwork2.vgtest simwork2.stdout.exp simwork2.stderr.exp \
@@ -31,7 +33,7 @@ EXTRA_DIST = \
3133
inline-crossfile.vgtest inline-crossfile.stderr.exp inline-crossfile.stdout.exp inline-crossfile.post.exp \
3234
inline-crossfile-helper1.h inline-crossfile-helper2.h filter_inline
3335

34-
check_PROGRAMS = clreq find_debuginfo simwork threads inline-samefile inline-crossfile
36+
check_PROGRAMS = clreq find_debuginfo simwork threads inline-samefile inline-crossfile runtime_obj_skip_c
3537

3638
AM_CFLAGS += $(AM_FLAG_M3264_PRI)
3739
AM_CXXFLAGS += $(AM_FLAG_M3264_PRI)
@@ -44,10 +46,21 @@ threads_LDADD = -lpthread
4446

4547
# Shim loaded by runtime_obj_skip_py.py via ctypes. Built unconditionally;
4648
# the test's prereq skips it if the .so is missing.
47-
check_DATA = runtime_obj_skip_py_shim.so
49+
check_DATA = runtime_obj_skip_py_shim.so runtime_obj_skip_c_lib.so
4850

4951
runtime_obj_skip_py_shim.so: runtime_obj_skip_py_shim.c
5052
$(CC) -shared -fPIC -O2 -I$(top_srcdir) -I$(top_srcdir)/include \
5153
$< -o $@
5254

53-
CLEANFILES = runtime_obj_skip_py_shim.so
55+
# Shared lib for the runtime_obj_skip_c test. Lives in a separate ELF
56+
# so the main binary can register its path for runtime obj-skip.
57+
runtime_obj_skip_c_lib.so: runtime_obj_skip_c_lib.c
58+
$(CC) -shared -fPIC -O2 -I$(top_srcdir) -I$(top_srcdir)/include \
59+
$< -o $@
60+
61+
runtime_obj_skip_c_LDADD = -ldl
62+
runtime_obj_skip_c_LDFLAGS = $(AM_LDFLAGS) -L. -l:runtime_obj_skip_c_lib.so \
63+
-Wl,-rpath,'$$ORIGIN'
64+
runtime_obj_skip_c_DEPENDENCIES = runtime_obj_skip_c_lib.so
65+
66+
CLEANFILES = runtime_obj_skip_py_shim.so runtime_obj_skip_c_lib.so
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* Minimal C reproducer for the runtime obj-skip leak: a fn from a
2+
* skipped object ends up as a top-level fn= block in the callgrind
3+
* output when it is the first BB instrumented after START.
4+
*
5+
* Strategy: register the lib for skip, then call into the lib BEFORE
6+
* starting instrumentation. The lib itself calls
7+
* CALLGRIND_START_INSTRUMENTATION mid-function, so the first BB
8+
* processed by callgrind lives in the skipped object — which trips
9+
* the (cxt == 0) push_cxt path that ignores the skip flag. */
10+
11+
#define _GNU_SOURCE
12+
#include <dlfcn.h>
13+
#include <stdio.h>
14+
#include "../callgrind.h"
15+
16+
extern void skipme_run(int n);
17+
18+
int main(void)
19+
{
20+
Dl_info info;
21+
if (dladdr((void*)skipme_run, &info) == 0 || !info.dli_fname) {
22+
fprintf(stderr, "dladdr failed\n");
23+
return 1;
24+
}
25+
CALLGRIND_ADD_OBJ_SKIP(info.dli_fname);
26+
27+
skipme_run(1000);
28+
29+
return 0;
30+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
OK

callgrind/tests/runtime_obj_skip_c.stderr.exp

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
prereq: test -f runtime_obj_skip_c && test -f runtime_obj_skip_c_lib.so
2+
prog-asis: ./runtime_obj_skip_c
3+
vgopts: --instr-atstart=no --compress-strings=no --callgrind-out-file=callgrind.out.runtime_obj_skip_c
4+
post: sh -c 'if grep -q "^fn=skipme_func" callgrind.out.runtime_obj_skip_c; then echo "FAIL: skipme_func leaked into top-level fn= block"; else echo OK; fi'
5+
cleanup: rm -f callgrind.out.runtime_obj_skip_c
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/* Library that lives in a separate ELF object so the main binary
2+
* can register its path for runtime obj-skip.
3+
*
4+
* skipme_run() flips instrumentation on from *inside* the skipped
5+
* object, then calls skipme_func. This is the trigger for the
6+
* `current_state.cxt == 0` push path in setup_bbcc: the very first
7+
* BB after instrumentation start lives in a skipped object, so the
8+
* (cxt==0) clause force-pushes a skipped fn as the new top context
9+
* and it leaks into the dump as a top-level fn= block. */
10+
11+
#include "../callgrind.h"
12+
13+
volatile long sink;
14+
15+
__attribute__((noinline))
16+
void skipme_func(int n)
17+
{
18+
for (int i = 0; i < n; i++) sink += i;
19+
}
20+
21+
__attribute__((noinline))
22+
void skipme_run(int n)
23+
{
24+
CALLGRIND_START_INSTRUMENTATION;
25+
skipme_func(n);
26+
CALLGRIND_STOP_INSTRUMENTATION;
27+
}

0 commit comments

Comments
 (0)