Skip to content

Commit 810fc4b

Browse files
authored
fix(profiler): close onThreadEnd teardown race (#552)
1 parent 857b926 commit 810fc4b

2 files changed

Lines changed: 46 additions & 20 deletions

File tree

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

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,19 +103,28 @@ void Profiler::onThreadEnd(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread) {
103103
_thread_filter.unregisterThread(slot_id);
104104
current->setFilterSlotId(-1);
105105
}
106-
107-
ProfiledThread::release();
108-
} else {
109-
// ProfiledThread already cleaned up - try to get tid from JVMTI as fallback
110-
tid = JVMThread::nativeThreadId(jni, thread);
111-
if (tid < 0) {
112-
// No ProfiledThread AND can't get tid from JVMTI - nothing we can do
113-
return;
106+
107+
updateThreadName(jvmti, jni, thread, false);
108+
// Block profiling signals around engine unregistration + TLS release to
109+
// close the window where a wall-clock/CPU signal could sample a
110+
// partially-torn-down thread (PROF-14674).
111+
{
112+
SignalBlocker blocker;
113+
_cpu_engine->unregisterThread(tid);
114+
_wall_engine->unregisterThread(tid);
115+
ProfiledThread::release();
114116
}
117+
return;
115118
}
116-
117-
// These can run if we have a valid tid
118-
updateThreadName(jvmti, jni, thread, false); // false = not self
119+
120+
// ProfiledThread already cleaned up - try to get tid from JVMTI as fallback
121+
tid = JVMThread::nativeThreadId(jni, thread);
122+
if (tid < 0) {
123+
// No ProfiledThread AND can't get tid from JVMTI - nothing we can do
124+
return;
125+
}
126+
127+
updateThreadName(jvmti, jni, thread, false);
119128
_cpu_engine->unregisterThread(tid);
120129
_wall_engine->unregisterThread(tid);
121130
}

ddprof-lib/src/test/cpp/thread_teardown_safety_ut.cpp

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,13 @@ TEST(ThreadTeardownSafetyTest, SignalSafeAccessorReturnsNullWithoutInit) {
175175

176176
// ── T-05: Concurrent signals during teardown stress ──────────────────────────
177177

178+
static std::atomic<int> g_t05_signal_count{0};
179+
180+
static void t05_handler(int) {
181+
(void)ProfiledThread::currentSignalSafe();
182+
g_t05_signal_count.fetch_add(1, std::memory_order_relaxed);
183+
}
184+
178185
static void *t05_worker(void *) {
179186
ProfiledThread::initCurrentThread();
180187
for (int i = 0; i < 10; ++i) {
@@ -184,14 +191,14 @@ static void *t05_worker(void *) {
184191
return nullptr;
185192
}
186193

187-
// 20 threads × 5 rounds with signal spray; no crash expected.
188-
// Signal dispositions are process-wide — set SIG_IGN once from the test body
189-
// before spawning workers so concurrent SigGuard restore races cannot re-expose
190-
// the default (terminate) disposition while a worker is mid-flight.
194+
// 20 threads × 5 rounds with signal spray; handler invocation is verified.
195+
// Signal dispositions are process-wide — install once from the test body so
196+
// no worker can race to restore SIG_DFL (terminate) mid-flight.
191197
TEST(ThreadTeardownSafetyTest, ConcurrentSignalsDuringTeardownStress) {
192198
SigGuard gVtalrm(SIGVTALRM);
193199
SigGuard gProf(SIGPROF);
194-
install_handler(SIGVTALRM, SIG_IGN);
200+
g_t05_signal_count.store(0, std::memory_order_relaxed);
201+
install_handler(SIGVTALRM, t05_handler);
195202
install_handler(SIGPROF, SIG_IGN);
196203
for (int round = 0; round < 5; ++round) {
197204
std::vector<pthread_t> threads(20);
@@ -202,6 +209,8 @@ TEST(ThreadTeardownSafetyTest, ConcurrentSignalsDuringTeardownStress) {
202209
pthread_join(tid, nullptr);
203210
}
204211
}
212+
EXPECT_GT(g_t05_signal_count.load(std::memory_order_relaxed), 0)
213+
<< "SIGVTALRM handler must have been invoked at least once";
205214
}
206215

207216
// ── T-06: SignalBlocker masks SIGPROF + SIGVTALRM and restores on exit ────────
@@ -335,6 +344,12 @@ TEST(ThreadTeardownSafetyTest, DoubleInitIsIdempotent) {
335344
// ── T-09: High-frequency signals during thread churn ─────────────────────────
336345

337346
static std::atomic<bool> g_t09_stop{false};
347+
static std::atomic<int> g_t09_signal_count{0};
348+
349+
static void t09_handler(int) {
350+
(void)ProfiledThread::currentSignalSafe();
351+
g_t09_signal_count.fetch_add(1, std::memory_order_relaxed);
352+
}
338353

339354
static void *t09_injector(void *) {
340355
while (!g_t09_stop.load(std::memory_order_relaxed)) {
@@ -345,19 +360,19 @@ static void *t09_injector(void *) {
345360
}
346361

347362
static void *t09_worker(void *) {
348-
SigGuard guard(SIGVTALRM);
349-
install_handler(SIGVTALRM, SIG_IGN);
350363
ProfiledThread::initCurrentThread();
351364
usleep(100);
352365
ProfiledThread::release();
353366
return nullptr;
354367
}
355368

356369
// Mirrors DumpWhileChurningThreadsTest at native level: 100 short-lived threads
357-
// under continuous SIGVTALRM injection must complete without crash.
370+
// under continuous SIGVTALRM injection must complete without crash; handler
371+
// invocation is verified.
358372
TEST(ThreadTeardownSafetyTest, HighFrequencySignalsDuringThreadChurn) {
359373
SigGuard testGuard(SIGVTALRM);
360-
install_handler(SIGVTALRM, SIG_IGN);
374+
g_t09_signal_count.store(0, std::memory_order_relaxed);
375+
install_handler(SIGVTALRM, t09_handler);
361376

362377
g_t09_stop.store(false, std::memory_order_relaxed);
363378
pthread_t injector;
@@ -371,6 +386,8 @@ TEST(ThreadTeardownSafetyTest, HighFrequencySignalsDuringThreadChurn) {
371386

372387
g_t09_stop.store(true, std::memory_order_relaxed);
373388
pthread_join(injector, nullptr);
389+
EXPECT_GT(g_t09_signal_count.load(std::memory_order_relaxed), 0)
390+
<< "SIGVTALRM handler must have been invoked at least once";
374391
}
375392

376393
// ── T-10: CriticalSection reentrancy guard prevents double-entry ──────────────

0 commit comments

Comments
 (0)