Skip to content

Commit 4809378

Browse files
committed
test: rewrite sysprof test using managed execution
Often, tests for sampling profiler require long running loops to be executed, so a certain situation is likely to occur. Thus, the test added in commit 285a1b0 ("sysprof: fix crash during FFUNC stream") expects the FFUNC VM state (and even the particular instruction to be executed) at the moment when stacktrace is being collected. Unfortunately, it leads to the test routine hang for several environments, and even if it does not, we cannot guarantee that the desired scenario is tested (only rely on statistics). As a result, the test for the aforementioned patch was disabled for Tarantool CI in the commit fef60a1 ("test: prevent hanging Tarantool CI by sysprof test") until the issue is not resolved. This patch introduces the new approach for testing our sampling profiler via <ptrace>, implementing precise managed execution of the profiled instance mentioned in tarantool/tarantool#9387. Instead of running around <tostring> gazillion times, we accurately step to the exact place where the issue reproduces and manually emit SIGPROF to the Lua VM being profiled. The particular approach implemented in this patch is described below. As it was mentioned, the test makes sysprof collect the particular event type (FFUNC) at the certain instruction in Lua VM (<lj_fff_res1>) to reproduce the issue from tarantool/tarantool#8594. Hence, it's enough to call <tostring> fast function in the profiled instance (i.e., "tracee"). To emit SIGPROF right at <lj_fff_res1> in the scope of <tostring> builtin, the manager (i.e., "tracer") is implemented. Here are the main steps (see comments and `man 2 ptrace' for more info): 1. Poison <int 3> instruction as the first instruction at <lj_ff_tostring> to stop at the beginning of the fast function; 2. Resume the "tracee" from the "tracer"; 3. Hit the emitted interruption, restore the original instruction and "rewind" the RIP to "replay" the instruction at <lj_ff_tostring>; 4. Do the hack 1-3 for <lj_fff_res1>; 5. Emit SIGPROF while resuming the "tracee"; As a result, sysprof collects the full backtrace with <tostring> fast function as the topmost frame. Resolves tarantool/tarantool#9387 Follows up tarantool/tarantool#8594 Reviewed-by: Maxim Kokryashkin <m.kokryashkin@tarantool.org> Reviewed-by: Sergey Kaplun <skaplun@tarantool.org> Signed-off-by: Igor Munkin <imun@tarantool.org> (cherry picked from commit caa9986)
1 parent 94ec9d8 commit 4809378

File tree

2 files changed

+284
-55
lines changed

2 files changed

+284
-55
lines changed
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
#include "lauxlib.h"
2+
#include "lmisclib.h"
3+
#include "lua.h"
4+
5+
#include "test.h"
6+
#include "utils.h"
7+
8+
#include <signal.h>
9+
#include <sys/ptrace.h>
10+
#include <sys/user.h>
11+
#include <sys/wait.h>
12+
#include <unistd.h>
13+
14+
/* XXX: Still need normal assert inside <tracee> and helpers. */
15+
#undef NDEBUG
16+
#include <assert.h>
17+
18+
/*
19+
* XXX: The test is *very* Linux/x86_64 specific. Fortunately, so
20+
* does the sampling profiler. <lj_arch.> is needed for LUAJIT_OS
21+
* and LUAJIT_TARGET.
22+
*/
23+
#include "lj_arch.h"
24+
25+
#if LUAJIT_OS == LUAJIT_OS_LINUX && LUAJIT_TARGET == LUAJIT_ARCH_X64
26+
27+
/*
28+
* XXX: The test makes sysprof collect the particular event
29+
* (FFUNC) at the particular instruction (<lj_fff_res1>) to
30+
* reproduce the issue #8594. Hence, it's enough to call
31+
* <tostring> fast function (this is done in <tracee> function).
32+
* To emit SIGPROF right at <lj_fff_res1> in scope of <tostring>
33+
* fast function, the managed execution is implemented in the
34+
* function <tracer>: <int 3> instruction is poisoned as the first
35+
* instruction at <lj_ff_tostring> to stop <tracee> at the
36+
* beginning of the fast function; <tracer> resumes the <tracee>;
37+
* the same hack is done for <lj_fff_res1>. When the <tracee> hits
38+
* the interruption at <lj_fff_res1>, SIGPROF is emitted while
39+
* resuming the <tracee>. As a result, sysprof collects the full
40+
* backtrace with <tostring> fast function as the topmost frame.
41+
*
42+
* See more info here:
43+
* * https://man7.org/linux/man-pages/man2/ptrace.2.html
44+
* * https://c9x.me/x86/html/file_module_x86_id_142.html
45+
* * https://github.com/tarantool/tarantool/issues/8594
46+
* * https://github.com/tarantool/tarantool/issues/9387
47+
*/
48+
49+
#define MESSAGE "Canary is alive"
50+
#define LUACALL "local a = tostring('" MESSAGE "') return a"
51+
52+
/* XXX: Resolve the necessary addresses from VM engine. */
53+
extern void *lj_ff_tostring(void);
54+
extern void *lj_fff_res1(void);
55+
56+
/* Sysprof dummy stream helpers. {{{ */
57+
58+
/*
59+
* Yep, 8Mb. Tuned in order not to bother the platform with too
60+
* often flushes.
61+
*/
62+
#define STREAM_BUFFER_SIZE (8 * 1024 * 1024)
63+
64+
struct dummy_ctx {
65+
/* Buffer for data recorded by sysprof. */
66+
uint8_t buf[STREAM_BUFFER_SIZE];
67+
};
68+
69+
static struct dummy_ctx context;
70+
71+
static int stream_new(struct luam_Sysprof_Options *options)
72+
{
73+
/* Set dummy context. */
74+
options->ctx = &context;
75+
options->buf = (uint8_t *)&context.buf;
76+
options->len = STREAM_BUFFER_SIZE;
77+
return PROFILE_SUCCESS;
78+
}
79+
80+
static int stream_delete(void *rawctx, uint8_t *buf)
81+
{
82+
assert(rawctx == &context);
83+
/* XXX: No need to release context memory. Just return. */
84+
return PROFILE_SUCCESS;
85+
}
86+
87+
static size_t stream_writer(const void **buf_addr, size_t len, void *rawctx)
88+
{
89+
assert(rawctx == &context);
90+
/* Do nothing, just return back to the profiler. */
91+
return STREAM_BUFFER_SIZE;
92+
}
93+
94+
/* }}} Sysprof dummy stream helpers. */
95+
96+
static int tracee(const char *luacode)
97+
{
98+
struct luam_Sysprof_Counters counters = {};
99+
struct luam_Sysprof_Options opt = {
100+
/* Collect full backtraces per event. */
101+
.mode = LUAM_SYSPROF_CALLGRAPH,
102+
/*
103+
* XXX: Setting the "endless timer". The test
104+
* requires the single event to be streamed at
105+
* <lj_fff_res1> instruction, so to avoid spoiling
106+
* the stream with other unwanted events, the
107+
* timer is set to some unreachable point, so the
108+
* profiler will be guaranteed to stop before any
109+
* event is emitted.
110+
*/
111+
.interval = -1ULL,
112+
};
113+
114+
/* Allow tracing for this process. */
115+
if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0) {
116+
perror("Failed to turn the calling process into a tracee");
117+
return EXIT_FAILURE;
118+
}
119+
120+
/*
121+
* XXX: Allow parent (which is our tracer now) to observe
122+
* our signal-delivery-stop (i.e. the tracee is ready).
123+
* For more info see ptrace(2), "Attaching and detaching".
124+
*/
125+
raise(SIGSTOP);
126+
127+
lua_State *L = utils_lua_init();
128+
129+
/* Customize and start profiler. */
130+
assert(stream_new(&opt) == PROFILE_SUCCESS);
131+
assert(luaM_sysprof_set_writer(stream_writer) == PROFILE_SUCCESS);
132+
assert(luaM_sysprof_set_on_stop(stream_delete) == PROFILE_SUCCESS);
133+
assert(luaM_sysprof_start(L, &opt) == PROFILE_SUCCESS);
134+
135+
/* TODO: Make this part test-agnostic. */
136+
assert(luaL_dostring(L, luacode) == LUA_OK);
137+
assert(strcmp(lua_tostring(L, -1), MESSAGE) == 0);
138+
139+
/* Terminate profiler. */
140+
assert(luaM_sysprof_stop(L) == PROFILE_SUCCESS);
141+
142+
/*
143+
* XXX: The only event to be streamed must be FFUNC at
144+
* the <lj_fff_res1> instruction.
145+
* TODO: Make this part test-agnostic.
146+
*/
147+
assert(luaM_sysprof_report(&counters) == PROFILE_SUCCESS);
148+
assert(counters.samples == 1);
149+
assert(counters.vmst_ffunc == 1);
150+
151+
/* Terminate Lua universe. */
152+
utils_lua_close(L);
153+
154+
return EXIT_SUCCESS;
155+
}
156+
157+
static void wait_alive(pid_t chpid)
158+
{
159+
int wstatus;
160+
161+
/* Wait <chpid> tracee signal-delivery-stop. */
162+
waitpid(chpid, &wstatus, 0);
163+
164+
/* Check the tracee is still alive and just stopped. */
165+
assert(!WIFEXITED(wstatus));
166+
assert(!WIFSIGNALED(wstatus));
167+
assert(WIFSTOPPED(wstatus));
168+
}
169+
170+
const uint8_t INT3 = 0xCC;
171+
static inline unsigned long int3poison(unsigned long instruction)
172+
{
173+
const size_t int3bits = sizeof(INT3) * 8;
174+
const unsigned long int3mask = -1UL >> int3bits << int3bits;
175+
return (instruction & int3mask) | INT3;
176+
}
177+
178+
static void continue_until(pid_t chpid, void *addr)
179+
{
180+
struct user_regs_struct regs;
181+
182+
/* Obtain the instructions at the <addr>. */
183+
unsigned long data = ptrace(PTRACE_PEEKTEXT, chpid, addr, NULL);
184+
/*
185+
* Emit the <int 3> instruction to the <addr>.
186+
* XXX: <int 3> is poisoned as the LSB to the <data>
187+
* obtained from the <addr> above.
188+
*/
189+
ptrace(PTRACE_POKETEXT, chpid, addr, int3poison(data));
190+
191+
/* Resume <chpid> tracee until SIGTRAP occurs. */
192+
ptrace(PTRACE_CONT, chpid, NULL, NULL);
193+
194+
/*
195+
* Wait <chpid> tracee signal-delivery-stop and check
196+
* whether it's still alive and just stopped via SIGTRAP.
197+
*/
198+
wait_alive(chpid);
199+
200+
/* Obtain GPR set to tweak RIP for further execution. */
201+
ptrace(PTRACE_GETREGS, chpid, NULL, &regs);
202+
/*
203+
* Make sure we indeed are stopped at <addr>.
204+
* XXX: RIP points right after <int 3> instruction.
205+
*/
206+
assert(regs.rip == (long)addr + sizeof(INT3));
207+
208+
/*
209+
* XXX: Restore the original instruction at <addr> and
210+
* "rewind" RIP by <int 3> size to "replay" the poisoned
211+
* instruction at the <addr>.
212+
*/
213+
regs.rip -= sizeof(INT3);
214+
ptrace(PTRACE_SETREGS, chpid, NULL, &regs);
215+
ptrace(PTRACE_POKETEXT, chpid, addr, data);
216+
}
217+
218+
static int tracer(pid_t chpid)
219+
{
220+
int wstatus;
221+
222+
/* Wait until <chpid> tracee is ready. */
223+
wait_alive(chpid);
224+
225+
/*
226+
* Resume <chpid> tracee until <lj_ff_tostring>.
227+
* The tracee is alive and stopped by SIGTRAP if
228+
* <continue_until> returns.
229+
*/
230+
continue_until(chpid, lj_ff_tostring);
231+
232+
/*
233+
* Resume <chpid> tracee until <lj_fff_res1>.
234+
* The tracee is alive and stopped by SIGTRAP if
235+
* <continue_until> returns.
236+
*/
237+
continue_until(chpid, lj_fff_res1);
238+
239+
/* Send SIGPROF to make sysprof collect the event. */
240+
ptrace(PTRACE_CONT, chpid, 0, SIGPROF);
241+
242+
/* Wait until <chpid> tracee successfully exits. */
243+
waitpid(chpid, &wstatus, 0);
244+
assert_true(WIFEXITED(wstatus));
245+
246+
return TEST_EXIT_SUCCESS;
247+
}
248+
249+
static int test_tostring_call(void *ctx)
250+
{
251+
pid_t chpid = fork();
252+
switch(chpid) {
253+
case -1:
254+
bail_out("Tracee fork failed");
255+
case 0:
256+
/*
257+
* XXX: Tracee has to <exit> instead of <return>
258+
* to avoid duplicate reports in <test_run_group>.
259+
* Test assertions are used only in the <tracer>,
260+
* so the <tracer> ought to report whether the
261+
* test succeeded or not.
262+
*/
263+
exit(tracee(LUACALL));
264+
default:
265+
return tracer(chpid);
266+
}
267+
}
268+
269+
#else /* LUAJIT_OS == LUAJIT_OS_LINUX && LUAJIT_TARGET == LUAJIT_ARCH_X64 */
270+
271+
static int test_tostring_call(void *ctx)
272+
{
273+
return skip_all("sysprof is implemented for Linux/x86_64 only");
274+
}
275+
276+
#endif /* LUAJIT_OS == LUAJIT_OS_LINUX && LUAJIT_TARGET == LUAJIT_ARCH_X64 */
277+
278+
int main(void)
279+
{
280+
const struct test_unit tgroup[] = {
281+
test_unit_def(test_tostring_call),
282+
};
283+
return test_run_group(tgroup, NULL);
284+
}

test/tarantool-tests/gh-8594-sysprof-ffunc-crash.test.lua

Lines changed: 0 additions & 55 deletions
This file was deleted.

0 commit comments

Comments
 (0)