Skip to content

Commit f243797

Browse files
committed
test(callgrind): C reproducer for cascading underflow obj-skip leak
Triggers the call-stack-underflow leak channel observed in the Python case (28 underflow events / run, almost all libpython interpreter frames). Mechanism: - Lib runs recursive skipme_recurse(N) with instrumentation OFF, so callgrind never sees the calls and its csp stays at 0. - At the deepest frame (n==0), CALLGRIND_START_INSTRUMENTATION fires. - Each RET on the way back hits csp == 0, triggers handleUnderflow, resets cxt to 0, and force-pushes the fn we're returning into. - Because that fn is in the skipped lib, it leaks as a top-level fn= block in the dump — N times for an N-deep recursion. With depth=5 the diagnostic logs show 1 (cxt==0) push + 6 underflow resets (5x skipme_recurse + 1x skipme_run), and the .out has fn=skipme_run and fn=skipme_recurse as top-level blocks.
1 parent 3934032 commit f243797

2 files changed

Lines changed: 59 additions & 0 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/* Driver for the underflow-channel obj-skip leak reproducer. */
2+
3+
#define _GNU_SOURCE
4+
#include <dlfcn.h>
5+
#include <stdio.h>
6+
#include "../callgrind.h"
7+
8+
extern void skipme_run(int depth);
9+
10+
int main(void)
11+
{
12+
Dl_info info;
13+
if (dladdr((void*)skipme_run, &info) == 0 || !info.dli_fname) {
14+
fprintf(stderr, "dladdr failed\n");
15+
return 1;
16+
}
17+
CALLGRIND_ADD_OBJ_SKIP(info.dli_fname);
18+
19+
skipme_run(5);
20+
21+
return 0;
22+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/* Library that triggers the call-stack-underflow leak channel in
2+
* callgrind obj-skip.
3+
*
4+
* Setup: recursive function in the skipped lib. Main calls in with
5+
* instrumentation OFF, so callgrind's call stack is never populated.
6+
* At the deepest frame, instrumentation is flipped ON. Each RET on
7+
* the way back then sees csp == 0, hits handleUnderflow, resets
8+
* cxt = 0, and force-pushes the current fn (which lives in the
9+
* skipped lib) as the new top context — leaking N times for an
10+
* N-deep stack.
11+
*
12+
* This is the same shape as Python 3.14's interpreter dispatch
13+
* leaks: deep recursive eval-loop frames where instrumentation was
14+
* started somewhere down the stack and every return pops past an
15+
* empty callgrind stack. */
16+
17+
#include "../callgrind.h"
18+
19+
volatile long sink;
20+
21+
__attribute__((noinline))
22+
void skipme_recurse(int n)
23+
{
24+
if (n == 0) {
25+
CALLGRIND_START_INSTRUMENTATION;
26+
return;
27+
}
28+
skipme_recurse(n - 1);
29+
sink += n;
30+
}
31+
32+
__attribute__((noinline))
33+
void skipme_run(int depth)
34+
{
35+
skipme_recurse(depth);
36+
CALLGRIND_STOP_INSTRUMENTATION;
37+
}

0 commit comments

Comments
 (0)