Skip to content

Commit 97e40c8

Browse files
authored
Merge branch 'main' into prof-15098-context-by-id
2 parents 9ac8760 + 54f6b41 commit 97e40c8

5 files changed

Lines changed: 690 additions & 149 deletions

File tree

ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp

Lines changed: 182 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <dlfcn.h>
1616
#include <mutex>
1717
#include <limits.h>
18+
#include <setjmp.h>
1819
#include <string.h>
1920
#include <stdlib.h>
2021

@@ -73,6 +74,166 @@ static void unregister_and_release(int tid) {
7374
ProfiledThread::release();
7475
}
7576

77+
// pthread_cleanup_push callback for thread wrappers.
78+
// Fires when the wrapped routine calls pthread_exit() or the thread is
79+
// canceled. Kept noinline so its stack frame (which may hold a SignalBlocker
80+
// via unregister_and_release) lives outside the DEOPT-corruption zone of the
81+
// caller on musl/aarch64, and so that the SignalBlocker's sigset_t does not
82+
// appear in the caller's frame on platforms with stack-protector canaries.
83+
__attribute__((noinline))
84+
static void cleanup_unregister(void*) {
85+
unregister_and_release(ProfiledThread::currentTid());
86+
}
87+
88+
// Thread-cleanup wrapper that avoids the static-libgcc / forced-unwind crash.
89+
//
90+
// The crash: on glibc, pthread_cleanup_push in C++ mode expands to
91+
// __pthread_cleanup_class (RAII), which adds a cleanup entry to the LSDA of
92+
// this frame. When libjavaProfiler.so is built with -static-libgcc, the
93+
// embedded __gxx_personality_v0 is called by the dynamic libgcc_s.so.1's
94+
// _Unwind_ForcedUnwind. The two libgcc versions have incompatible
95+
// _Unwind_Context layouts; calling _Unwind_SetGR (which happens when the
96+
// personality finds a cleanup action) with a cross-version context triggers
97+
// the cold/error path, which calls abort().
98+
//
99+
// The fix: use __pthread_register_cancel / __pthread_unregister_cancel
100+
// directly — the same thing the C macro form of pthread_cleanup_push does.
101+
// This registers cleanup via a setjmp buffer in a runtime linked-list, NOT
102+
// via an LSDA destructor. _Unwind_ForcedUnwind's stop function
103+
// (__pthread_unwind_stop) handles the cleanup without ever calling
104+
// __gxx_personality_v0 for this frame, so _Unwind_SetGR is never called and
105+
// the cross-version incompatibility is never triggered.
106+
//
107+
// On musl: pthread_cleanup_push already uses the C/setjmp form (no RAII),
108+
// and pthread_exit does not use _Unwind_ForcedUnwind, so there is no issue.
109+
// The __GLIBC__ guard keeps the musl path unchanged.
110+
#ifdef __GLIBC__
111+
// On glibc, <pthread.h> declares __pthread_register_cancel etc. only inside
112+
// the C (non-C++) conditional, so they're invisible in C++ code. Redeclare
113+
// them with extern "C" so we can call them directly without the header guard.
114+
extern "C" {
115+
extern void __pthread_register_cancel(__pthread_unwind_buf_t*);
116+
extern void __pthread_unregister_cancel(__pthread_unwind_buf_t*);
117+
[[noreturn]] extern void __pthread_unwind_next(__pthread_unwind_buf_t*);
118+
}
119+
#endif
120+
121+
__attribute__((visibility("hidden"), noinline, no_stack_protector))
122+
void run_with_cleanup(func_start_routine routine, void* params,
123+
void (*cleanup_fn)(void*), void* cleanup_arg) {
124+
#ifdef __GLIBC__
125+
__pthread_unwind_buf_t cancel_buf = {};
126+
// With savemask=0, __sigsetjmp only writes __jmp_buf + int __mask_was_saved;
127+
// it never touches __saved_mask. The inner struct of __pthread_unwind_buf_t
128+
// must cover exactly that writable prefix of struct __jmp_buf_tag.
129+
static_assert(offsetof(__pthread_unwind_buf_t, __cancel_jmp_buf) == 0 &&
130+
sizeof(cancel_buf.__cancel_jmp_buf[0]) == offsetof(struct __jmp_buf_tag, __saved_mask),
131+
"glibc __pthread_unwind_buf_t inner layout incompatible with struct __jmp_buf_tag");
132+
// __sigsetjmp/longjmp only intercepts _Unwind_ForcedUnwind (pthread_exit /
133+
// cancellation). routine(params) must NOT throw a regular C++ exception
134+
// across this boundary: an escaping exception would skip both
135+
// __pthread_unregister_cancel and cleanup_fn below, leaking the thread
136+
// registration and leaving cancel_buf linked against this (unwound) frame.
137+
// We cannot defend with a try/catch here — a handler frame adds an LSDA
138+
// action, which is exactly what triggers the static-libgcc abort this
139+
// function exists to avoid. Production routines are JVM/native start
140+
// routines that handle their own exceptions and do not throw across here.
141+
if (__builtin_expect(
142+
// set __sigsetjmp's savemask=0 (the second parameter, noting that the signal mask is NOT
143+
// saved/restored, which is correct because the cancel mechanism does not depend on signal mask state.
144+
__sigsetjmp((struct __jmp_buf_tag*)(void*)cancel_buf.__cancel_jmp_buf, 0), 0)) {
145+
// Reached via longjmp from glibc's stop function when pthread_exit
146+
// (or cancellation) fires. Run cleanup and continue unwinding.
147+
cleanup_fn(cleanup_arg);
148+
__pthread_unwind_next(&cancel_buf);
149+
// __pthread_unwind_next is [[noreturn]]; this fails loudly rather than
150+
// falling through into __pthread_register_cancel on a torn-down frame
151+
// should a future/variant glibc ever return from it.
152+
__builtin_unreachable();
153+
}
154+
// Callers must not have a pending pthread_cancel when they enter
155+
// run_with_cleanup: a cancellation arriving between __sigsetjmp returning
156+
// and __pthread_register_cancel below would unwind this frame before
157+
// cancel_buf is registered, silently skipping cleanup_fn. All current
158+
// callers are JVM/native start routines with no pending cancellation.
159+
__pthread_register_cancel(&cancel_buf);
160+
routine(params);
161+
__pthread_unregister_cancel(&cancel_buf);
162+
cleanup_fn(cleanup_arg);
163+
#else
164+
// musl / non-glibc: pthread_cleanup_push uses the C/setjmp form, no RAII.
165+
pthread_cleanup_push(cleanup_fn, cleanup_arg);
166+
routine(params);
167+
pthread_cleanup_pop(1);
168+
#endif
169+
}
170+
171+
#ifdef UNIT_TEST
172+
// Integration test entry point: exercises the full start_routine_wrapper →
173+
// run_with_cleanup chain without calling Profiler::registerThread or
174+
// Profiler::unregisterThread, which dereference _cpu_engine/_wall_engine and
175+
// crash when the profiler is not started (as in gtest).
176+
//
177+
// The caller supplies cleanup_fn/cleanup_arg so the test can verify cleanup
178+
// fires and observe ProfiledThread::release() without coupling to Profiler state.
179+
//
180+
// Thread lifecycle:
181+
// pthread_create_wrapped_for_test → start_routine_for_test
182+
// → ProfiledThread::initCurrentThread()
183+
// → run_with_cleanup(routine, params, cleanup_fn, cleanup_arg)
184+
// → pthread_exit(nullptr)
185+
struct WrapperTestCtx {
186+
func_start_routine routine;
187+
void* params;
188+
void (*cleanup_fn)(void*);
189+
void* cleanup_arg;
190+
};
191+
192+
__attribute__((visibility("hidden"), noinline, no_stack_protector))
193+
static void* start_routine_for_test(void* raw) {
194+
auto* ctx = static_cast<WrapperTestCtx*>(raw);
195+
func_start_routine routine = ctx->routine;
196+
void* params = ctx->params;
197+
void (*cleanup_fn)(void*) = ctx->cleanup_fn;
198+
void* cleanup_arg = ctx->cleanup_arg;
199+
{
200+
SignalBlocker blocker;
201+
delete ctx;
202+
ProfiledThread::initCurrentThread();
203+
}
204+
run_with_cleanup(routine, params, cleanup_fn, cleanup_arg);
205+
pthread_exit(nullptr);
206+
__builtin_unreachable();
207+
}
208+
209+
int pthread_create_wrapped_for_test(pthread_t* thread,
210+
func_start_routine routine, void* params,
211+
void (*cleanup_fn)(void*), void* cleanup_arg) {
212+
WrapperTestCtx* ctx;
213+
{
214+
SignalBlocker blocker;
215+
ctx = new WrapperTestCtx{routine, params, cleanup_fn, cleanup_arg};
216+
}
217+
int ret = pthread_create(thread, nullptr, start_routine_for_test, ctx);
218+
if (ret != 0) {
219+
SignalBlocker blocker;
220+
delete ctx;
221+
}
222+
return ret;
223+
}
224+
225+
// Variant that passes the production cleanup_unregister as the cleanup function.
226+
// Exercises the full chain: start_routine_for_test → run_with_cleanup →
227+
// cleanup_unregister → Profiler::unregisterThread + ProfiledThread::release.
228+
// Profiler::unregisterThread is null-safe under UNIT_TEST (see profiler.cpp).
229+
int pthread_create_with_cleanup_unregister_for_test(pthread_t* thread,
230+
func_start_routine routine,
231+
void* params) {
232+
return pthread_create_wrapped_for_test(thread, routine, params,
233+
cleanup_unregister, nullptr);
234+
}
235+
#endif // UNIT_TEST
236+
76237
#ifdef __aarch64__
77238
// Delete RoutineInfo with profiling signals blocked to prevent ASAN
78239
// allocator lock reentrancy. Kept noinline so SignalBlocker's sigset_t
@@ -99,29 +260,6 @@ static void init_tls_and_register() {
99260
Profiler::registerThread(ProfiledThread::currentTid());
100261
}
101262

102-
// pthread_cleanup_push callback for start_routine_wrapper_spec.
103-
// Fires when the wrapped routine calls pthread_exit() or the thread is
104-
// canceled. Kept noinline so its stack frame (which may hold a SignalBlocker
105-
// via unregister_and_release) lives outside the DEOPT-corruption zone of
106-
// start_routine_wrapper_spec.
107-
__attribute__((noinline))
108-
static void cleanup_unregister(void*) {
109-
unregister_and_release(ProfiledThread::currentTid());
110-
}
111-
112-
// pthread_cleanup_push declares `struct __ptcb` in the caller's frame. If that
113-
// frame is start_routine_wrapper_spec, the structure sits inside the ~224-byte
114-
// DEOPT-corruption zone and pthread_cleanup_pop(1) would invoke a clobbered
115-
// function pointer. This noinline + no_stack_protector helper hoists the
116-
// cleanup-handler frame out of the corruption zone — its own frame lives
117-
// safely above start_routine_wrapper_spec's.
118-
__attribute__((noinline, no_stack_protector))
119-
static void run_with_musl_cleanup(func_start_routine routine, void* params) {
120-
pthread_cleanup_push(cleanup_unregister, nullptr);
121-
routine(params);
122-
pthread_cleanup_pop(1);
123-
}
124-
125263
// Wrapper around the real start routine.
126264
// The wrapper:
127265
// 1. Register the newly created thread to profiler
@@ -172,11 +310,20 @@ static void* start_routine_wrapper_spec(void* args) {
172310
delete_routine_info(thr);
173311
init_tls_and_register();
174312
// cleanup_unregister fires on pthread_exit() or cancellation from within
175-
// routine(params). The push/pop pair lives inside run_with_musl_cleanup so
176-
// that `struct __ptcb` does not land in this frame's DEOPT-corruption zone.
177-
run_with_musl_cleanup(routine, params);
313+
// routine(params). The push/pop pair lives inside run_with_cleanup so
314+
// that __pthread_unwind_buf_t (glibc) / struct __ptcb (musl) does not land
315+
// in this frame's DEOPT-corruption zone.
316+
run_with_cleanup(routine, params, cleanup_unregister, nullptr);
178317
// pthread_exit instead of 'return': the saved LR in this frame is corrupted
179318
// by DEOPT PACKING; returning would jump to a garbage address.
319+
// cleanup_unregister has already run via run_with_cleanup's normal return
320+
// path, so there is no registered cancel handler left. The forced unwind
321+
// raised by pthread_exit walks this frame, but it is safe because no
322+
// destructor-bearing local (and hence no LSDA cleanup/handler action) is
323+
// live at this call site: __gxx_personality_v0 returns continue-unwind
324+
// without ever calling _Unwind_SetGR, avoiding the static-libgcc abort.
325+
// WARNING: adding any RAII local with a destructor between run_with_cleanup
326+
// and pthread_exit would reintroduce that crash.
180327
pthread_exit(nullptr);
181328
__builtin_unreachable();
182329
}
@@ -202,7 +349,7 @@ static int pthread_create_hook_spec(pthread_t* thread,
202349

203350
// Wrapper around the real start routine.
204351
// See comments for start_routine_wrapper_spec() for details
205-
__attribute__((visibility("hidden")))
352+
__attribute__((visibility("hidden"), no_stack_protector))
206353
static void* start_routine_wrapper(void* args) {
207354
RoutineInfo* thr = (RoutineInfo*)args;
208355
func_start_routine routine;
@@ -227,14 +374,14 @@ static void* start_routine_wrapper(void* args) {
227374
ProfiledThread::currentSignalSafe()->startInitWindow();
228375
Profiler::registerThread(ProfiledThread::currentTid());
229376
}
230-
// RAII cleanup: reads tid from TLS in the destructor (same rationale as
231-
// start_routine_wrapper_spec: avoids storing state on a potentially corruptible frame).
232-
// unregister_and_release() wraps the two calls under SignalBlocker (PROF-14603).
233-
struct Cleanup {
234-
~Cleanup() { unregister_and_release(ProfiledThread::currentTid()); }
235-
} cleanup;
236-
routine(params);
237-
return nullptr;
377+
// Use POSIX cleanup instead of C++ RAII to handle pthread_exit(): see run_with_cleanup.
378+
// cleanup_unregister has already run on run_with_cleanup's normal return path.
379+
// The pthread_exit forced unwind is safe here for the same reason as in
380+
// start_routine_wrapper_spec: no destructor-bearing local is live at this
381+
// call site, so __gxx_personality_v0 never calls _Unwind_SetGR.
382+
run_with_cleanup(routine, params, cleanup_unregister, nullptr);
383+
pthread_exit(nullptr);
384+
__builtin_unreachable();
238385
}
239386

240387
static int pthread_create_hook(pthread_t* thread,

ddprof-lib/src/main/cpp/profiler.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,28 @@ int Profiler::registerThread(int tid) {
133133
return _instance->_cpu_engine->registerThread(tid) |
134134
_instance->_wall_engine->registerThread(tid);
135135
}
136+
#ifdef UNIT_TEST
137+
static std::atomic<int> g_test_last_unregistered_tid{-1};
138+
139+
int Profiler::lastUnregisteredTidForTest() {
140+
return g_test_last_unregistered_tid.load(std::memory_order_relaxed);
141+
}
142+
void Profiler::resetUnregisterObservableForTest() {
143+
g_test_last_unregistered_tid.store(-1, std::memory_order_relaxed);
144+
}
145+
#endif
146+
136147
void Profiler::unregisterThread(int tid) {
148+
#ifdef UNIT_TEST
149+
// In gtest, _cpu_engine/_wall_engine are null (profiler not started).
150+
// Record the tid so integration tests can verify the call happened without
151+
// crashing on the null engine dereference. This bypasses the real engine
152+
// unregister path entirely, so that path is covered only by JVM-level tests,
153+
// not by these gtests. UNIT_TEST is defined solely for the gtest binaries
154+
// (see GtestTaskBuilder); the shipped library never compiles this branch.
155+
g_test_last_unregistered_tid.store(tid, std::memory_order_relaxed);
156+
return;
157+
#endif
137158
_instance->_cpu_engine->unregisterThread(tid);
138159
_instance->_wall_engine->unregisterThread(tid);
139160
}

ddprof-lib/src/main/cpp/profiler.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,15 @@ class alignas(alignof(SpinLock)) Profiler {
403403
static int registerThread(int tid);
404404
static void unregisterThread(int tid);
405405

406+
#ifdef UNIT_TEST
407+
// Returns the tid most recently passed to unregisterThread(), or -1 if it
408+
// has never been called (or since the last resetUnregisterObservableForTest).
409+
// Used by integration tests to assert that cleanup_unregister wired
410+
// Profiler::unregisterThread correctly without needing live engine instances.
411+
static int lastUnregisteredTidForTest();
412+
static void resetUnregisterObservableForTest();
413+
#endif
414+
406415

407416
static void JNICALL ThreadStart(jvmtiEnv *jvmti, JNIEnv *jni,
408417
jthread thread) {

0 commit comments

Comments
 (0)