Skip to content

Commit 693caf2

Browse files
authored
Fix JVM crash when Thread::current() returns nullptr (PROF-13072) (#461)
1 parent f9ea8f2 commit 693caf2

5 files changed

Lines changed: 59 additions & 7 deletions

File tree

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "guards.h"
2121
#include "ctimer.h"
2222
#include "debugSupport.h"
23+
#include "jvmThread.h"
2324
#include "libraries.h"
2425
#include "profiler.h"
2526
#include "threadState.inline.h"
@@ -156,6 +157,16 @@ void CTimer::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
156157
int tid = 0;
157158
ProfiledThread *current = ProfiledThread::currentSignalSafe();
158159
assert(current == nullptr || !current->isDeepCrashHandler());
160+
// Guard against the race window between Profiler::registerThread() and
161+
// thread_native_entry setting JVM TLS (PROF-13072): skip at most one signal
162+
// per thread. Pure native threads (where JVMThread::current() is always null)
163+
// are allowed through once the one-shot window expires.
164+
if (current != nullptr && JVMThread::isInitialized() && JVMThread::current() == nullptr
165+
&& current->inInitWindow()) {
166+
current->tickInitWindow();
167+
errno = saved_errno;
168+
return;
169+
}
159170
if (current != NULL) {
160171
current->noteCPUSample(Profiler::instance()->recordingEpoch());
161172
tid = current->tid();

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

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,19 @@ static void init_thread_tls() {
7272
ProfiledThread::initCurrentThread();
7373
}
7474

75+
// Arm the CPU timer with profiling signals blocked and open the init window
76+
// (PROF-13072). Kept noinline for the same stack-protector reason as
77+
// delete_routine_info: SignalBlocker's sigset_t must not appear in
78+
// start_routine_wrapper_spec's own stack frame on musl/aarch64.
79+
__attribute__((noinline))
80+
static void start_window_and_register() {
81+
SignalBlocker blocker;
82+
if (ProfiledThread *pt = ProfiledThread::currentSignalSafe()) {
83+
pt->startInitWindow();
84+
}
85+
Profiler::registerThread(ProfiledThread::currentTid());
86+
}
87+
7588
// Wrapper around the real start routine.
7689
// The wrapper:
7790
// 1. Register the newly created thread to profiler
@@ -86,10 +99,9 @@ static void* start_routine_wrapper_spec(void* args) {
8699
void* params = thr->args();
87100
delete_routine_info(thr);
88101
init_thread_tls();
89-
int tid = ProfiledThread::currentTid();
90-
Profiler::registerThread(tid);
102+
start_window_and_register();
91103
void* result = routine(params);
92-
Profiler::unregisterThread(tid);
104+
Profiler::unregisterThread(ProfiledThread::currentTid());
93105
ProfiledThread::release();
94106
return result;
95107
}
@@ -126,21 +138,28 @@ static void* start_routine_wrapper(void* args) {
126138
RoutineInfo* thr = (RoutineInfo*)args;
127139
func_start_routine routine;
128140
void* params;
141+
int tid;
129142
{
130143
// Block profiling signals while accessing and freeing RoutineInfo
131144
// and during TLS initialization. Under ASAN, new/delete/
132145
// pthread_setspecific are intercepted and acquire ASAN's internal
133146
// allocator lock. A profiling signal during any of these calls
134147
// runs ASAN-instrumented code that tries to acquire the same
135148
// lock, causing deadlock.
149+
// registerThread is also kept inside the blocker so that the CPU
150+
// timer is armed while SIGPROF/SIGVTALRM are masked. Any pending
151+
// signal fires only after signals are re-enabled (when the blocker
152+
// scope exits), at which point JVMThread::current() is still null
153+
// and the guard in CTimer::signalHandler discards the sample safely.
136154
SignalBlocker blocker;
137155
routine = thr->routine();
138156
params = thr->args();
139157
delete thr;
140158
ProfiledThread::initCurrentThread();
159+
tid = ProfiledThread::currentTid();
160+
ProfiledThread::currentSignalSafe()->startInitWindow();
161+
Profiler::registerThread(tid);
141162
}
142-
int tid = ProfiledThread::currentTid();
143-
Profiler::registerThread(tid);
144163
void* result = nullptr;
145164
// Handle pthread_exit() bypass - the thread calls pthread_exit()
146165
// instead of normal termination

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ void ProfiledThread::releaseFromBuffer() {
9999
_call_trace_id = 0;
100100
_recording_epoch = 0;
101101
_filter_slot_id = -1;
102+
_init_window = 0;
102103
_unwind_failures.clear();
103104

104105
// Null the TLS pointer so external profilers that dereference the pointer

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class ProfiledThread : public ThreadLocalData {
6969
u32 _recording_epoch;
7070
u32 _misc_flags;
7171
int _filter_slot_id; // Slot ID for thread filtering
72+
uint8_t _init_window; // Countdown for JVM thread init race window (PROF-13072)
7273
UnwindFailures _unwind_failures;
7374
bool _otel_ctx_initialized;
7475
bool _crash_protection_active;
@@ -81,7 +82,8 @@ class ProfiledThread : public ThreadLocalData {
8182

8283
ProfiledThread(int buffer_pos, int tid)
8384
: ThreadLocalData(), _pc(0), _sp(0), _span_id(0), _crash_depth(0), _buffer_pos(buffer_pos), _tid(tid), _cpu_epoch(0),
84-
_wall_epoch(0), _call_trace_id(0), _recording_epoch(0), _misc_flags(0), _filter_slot_id(-1), _otel_ctx_initialized(false), _crash_protection_active(false),
85+
_wall_epoch(0), _call_trace_id(0), _recording_epoch(0), _misc_flags(0), _filter_slot_id(-1), _init_window(0),
86+
_otel_ctx_initialized(false), _crash_protection_active(false),
8587
_otel_ctx_record{}, _otel_tag_encodings{}, _otel_local_root_span_id(0) {};
8688

8789
virtual ~ProfiledThread() { }
@@ -180,7 +182,16 @@ class ProfiledThread : public ThreadLocalData {
180182

181183
int filterSlotId() { return _filter_slot_id; }
182184
void setFilterSlotId(int slotId) { _filter_slot_id = slotId; }
183-
185+
186+
// JVM thread init race window (PROF-13072): skip at most one signal that fires
187+
// between Profiler::registerThread() and the JVM's pd_set_thread() call.
188+
// Pure native threads (e.g. NativeThreadCreator) also see nullptr from
189+
// JVMThread::current(), so the window auto-expires after one skip, allowing
190+
// their subsequent samples through.
191+
inline bool inInitWindow() const { return _init_window > 0; }
192+
inline void startInitWindow() { _init_window = 1; }
193+
inline void tickInitWindow() { if (_init_window > 0) --_init_window; }
194+
184195
// Signal handler reentrancy protection
185196
bool tryEnterCriticalSection() {
186197
// Uses GCC atomic builtin (no malloc, async-signal-safe)

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "context.h"
1111
#include "context_api.h"
1212
#include "debugSupport.h"
13+
#include "jvmThread.h"
1314
#include "libraries.h"
1415
#include "log.h"
1516
#include "profiler.h"
@@ -64,6 +65,15 @@ void WallClockASGCT::signalHandler(int signo, siginfo_t *siginfo, void *ucontext
6465
return; // Another critical section is active, defer profiling
6566
}
6667
ProfiledThread *current = ProfiledThread::currentSignalSafe();
68+
// Guard against the race window between Profiler::registerThread() and
69+
// thread_native_entry setting JVM TLS (PROF-13072): skip at most one signal
70+
// per thread. Pure native threads (where JVMThread::current() is always null)
71+
// are allowed through once the one-shot window expires.
72+
if (current != nullptr && JVMThread::isInitialized() && JVMThread::current() == nullptr
73+
&& current->inInitWindow()) {
74+
current->tickInitWindow();
75+
return;
76+
}
6777
int tid = current != NULL ? current->tid() : OS::threadId();
6878
Shims::instance().setSighandlerTid(tid);
6979
u64 call_trace_id = 0;

0 commit comments

Comments
 (0)