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 ))
206353static 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
240387static int pthread_create_hook (pthread_t * thread,
0 commit comments