Skip to content

Commit c207f9d

Browse files
committed
v7
1 parent aa8f89d commit c207f9d

4 files changed

Lines changed: 168 additions & 147 deletions

File tree

ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,6 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
182182
bool fp_chain_fallback = false;
183183
int fp_chain_depth = 0;
184184

185-
ProfiledThread* profiled_thread = ProfiledThread::currentSignalSafe();
186-
187185
_jmp_ctx.set(&crash_protection_ctx);
188186
if (setjmp(crash_protection_ctx) != 0) {
189187
// checkFault() does a longjmp from inside segvHandler, bypassing

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,11 @@ class ProfiledThread : public ThreadLocalData {
2828
TYPE_MASK = TYPE_JAVA_THREAD | TYPE_NOT_JAVA_THREAD
2929
};
3030

31-
private:
32-
// We are allowing several levels of nesting because we can be
33-
// eg. in a crash handler when wallclock signal kicks in,
34-
// catching sigseg while also triggering CPU signal handler
35-
// which would also potentially trigger sigseg we need to handle.
36-
// This means 3 levels but we allow for some wiggling space, just in case.
37-
// Even with 5 levels cap we will need any highly recursing signal handlers
31+
// Maximum number of nested crash-handler invocations allowed on a single
32+
// thread. Exposed publicly so unit tests can loop to the limit precisely.
3833
static constexpr u32 CRASH_HANDLER_NESTING_LIMIT = 5;
34+
35+
private:
3936
static pthread_key_t _tls_key;
4037
static bool _tls_key_initialized;
4138

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

Lines changed: 134 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,41 @@
22
* Copyright 2026 Datadog, Inc.
33
* SPDX-License-Identifier: Apache-2.0
44
*
5-
* Unit tests for the MonitorDeflationThread crash-protection gate.
5+
* Unit tests for the crash-protection and thread-classification mechanisms
6+
* used by HotspotSupport::walkVM().
67
*
7-
* Root cause (JDK 25.0.2): walkVM() unconditionally wrote a jmp_buf address
8-
* into ThreadShadow::_exception_file for every non-null VMThread, including
9-
* JVM-internal threads like MonitorDeflationThread. In JDK 25,
10-
* ObjectMonitorDeflationSafepointer reads _exception_file at safepoint
11-
* boundaries; a stale jmp_buf address caused a crash in deflate_monitor_list.
8+
* Background: profiling signals can interrupt any JVM thread — including
9+
* JVM-internal threads such as MonitorDeflationThread. walkVM must never
10+
* dereference JavaThread-only fields (anchor, vframe_top, …) on such threads.
11+
* VMThread::isJavaThread() provides the gate.
1212
*
13-
* Fix: gate the write on `VMThread::isJavaThread(vm_thread)`.
13+
* Crash recovery inside walkVM relies on setjmp/longjmp:
14+
* 1. walkVM stores a jmp_buf* in HotspotSupport::_jmp_ctx (thread-local).
15+
* 2. If a fault fires during the walk, checkFault() detects the live context
16+
* via isThreadProtectedByLongjmp() and calls longjmp() to unwind.
17+
* 3. ProfiledThread tracks nested crash-handler depth so recursive faults
18+
* (e.g. wall-clock signal inside a crash handler) are capped safely.
1419
*
15-
* `isJavaThread()` has two paths:
16-
* 1. Fast path — reads the cached ProfiledThread::ThreadType set by the
17-
* JVMTI ThreadStart callback (TYPE_JAVA_THREAD /
18-
* TYPE_NOT_JAVA_THREAD).
19-
* 2. Slow path — falls back to the vtable majority vote in
20-
* VMThread::hasJavaThreadVtable(): at least 2 of the 3
21-
* selected vtable entries (indices 1, 3, 5) must match
22-
* those of a known JavaThread captured at profiler start.
23-
*
24-
* Tests here cover:
25-
* A. ProfiledThread classification (fast path) — setJavaThread / threadType.
26-
* B. Vtable majority-vote logic (slow path) — replicated inline as a
27-
* whitebox test because hasJavaThreadVtable() is private.
28-
* C. setup_crash_protection gate condition — the boolean that walkVM
29-
* uses to decide whether to touch _exception_file.
20+
* Tests cover:
21+
* A. ProfiledThread thread-type classification (isJavaThread fast path)
22+
* B. Vtable majority-vote logic (isJavaThread slow path)
23+
* C. Crash-handler nesting depth (ProfiledThread crash handler state)
24+
* D. Longjmp-protection state via HotspotSupport public API
3025
*/
3126

3227
#include <gtest/gtest.h>
3328
#include "thread.h"
29+
#include "hotspot/hotspotSupport.h"
3430

3531
#ifdef __linux__
3632

37-
#include <pthread.h>
3833
#include <cstring>
3934

4035
// ---------------------------------------------------------------------------
41-
// A. ProfiledThread thread-type classification (fast path of isJavaThread)
36+
// A. ProfiledThread thread-type classification (isJavaThread fast path)
37+
//
38+
// JVMTI ThreadStart callbacks call setJavaThread(true/false) to cache the
39+
// result; isJavaThread() returns it directly without touching the vtable.
4240
// ---------------------------------------------------------------------------
4341

4442
class ProfiledThreadTypeTest : public ::testing::Test {
@@ -56,26 +54,22 @@ class ProfiledThreadTypeTest : public ::testing::Test {
5654
ProfiledThread* _pt = nullptr;
5755
};
5856

59-
// A fresh ProfiledThread carries TYPE_UNKNOWN until explicitly classified.
57+
// A fresh ProfiledThread is not yet classified.
6058
TEST_F(ProfiledThreadTypeTest, InitialStateIsUnknown) {
6159
EXPECT_EQ(ProfiledThread::TYPE_UNKNOWN, _pt->threadType());
6260
}
6361

64-
// After being marked as a Java thread the type is TYPE_JAVA_THREAD.
6562
TEST_F(ProfiledThreadTypeTest, MarkAsJavaThreadSetsCorrectType) {
6663
_pt->setJavaThread(true);
6764
EXPECT_EQ(ProfiledThread::TYPE_JAVA_THREAD, _pt->threadType());
6865
}
6966

70-
// After being marked as a non-Java thread (e.g. MonitorDeflationThread) the
71-
// type is TYPE_NOT_JAVA_THREAD, which makes isJavaThread() return false and
72-
// prevents walkVM from writing _exception_file.
67+
// MonitorDeflationThread and similar JVM-internal threads are marked false.
7368
TEST_F(ProfiledThreadTypeTest, MarkAsNonJavaThreadSetsCorrectType) {
7469
_pt->setJavaThread(false);
7570
EXPECT_EQ(ProfiledThread::TYPE_NOT_JAVA_THREAD, _pt->threadType());
7671
}
7772

78-
// Reclassification works: a thread initially marked Java can be reclassified.
7973
TEST_F(ProfiledThreadTypeTest, ReclassificationFromJavaToNonJava) {
8074
_pt->setJavaThread(true);
8175
EXPECT_EQ(ProfiledThread::TYPE_JAVA_THREAD, _pt->threadType());
@@ -84,44 +78,35 @@ TEST_F(ProfiledThreadTypeTest, ReclassificationFromJavaToNonJava) {
8478
EXPECT_EQ(ProfiledThread::TYPE_NOT_JAVA_THREAD, _pt->threadType());
8579
}
8680

87-
// The fast-path short-circuit: if threadType() != TYPE_UNKNOWN,
88-
// isJavaThread() returns it directly without consulting the vtable.
89-
// Verify the logic isJavaThread() uses:
90-
// type != TYPE_UNKNOWN → return type == TYPE_JAVA_THREAD
91-
TEST_F(ProfiledThreadTypeTest, FastPathLogicForJavaThread) {
81+
// Replicate the fast-path branch used by isJavaThread():
82+
// if (type != TYPE_UNKNOWN) return type == TYPE_JAVA_THREAD;
83+
TEST_F(ProfiledThreadTypeTest, FastPathReturnsTrueForJavaThread) {
9284
_pt->setJavaThread(true);
9385
ProfiledThread::ThreadType type = _pt->threadType();
94-
bool fast_path_result = (type != ProfiledThread::TYPE_UNKNOWN)
95-
&& (type == ProfiledThread::TYPE_JAVA_THREAD);
96-
EXPECT_TRUE(fast_path_result);
86+
bool result = (type != ProfiledThread::TYPE_UNKNOWN)
87+
&& (type == ProfiledThread::TYPE_JAVA_THREAD);
88+
EXPECT_TRUE(result);
9789
}
9890

99-
TEST_F(ProfiledThreadTypeTest, FastPathLogicForNonJavaThread) {
91+
TEST_F(ProfiledThreadTypeTest, FastPathReturnsFalseForNonJavaThread) {
10092
_pt->setJavaThread(false);
10193
ProfiledThread::ThreadType type = _pt->threadType();
102-
bool fast_path_result = (type != ProfiledThread::TYPE_UNKNOWN)
103-
&& (type == ProfiledThread::TYPE_JAVA_THREAD);
104-
EXPECT_FALSE(fast_path_result);
94+
bool result = (type != ProfiledThread::TYPE_UNKNOWN)
95+
&& (type == ProfiledThread::TYPE_JAVA_THREAD);
96+
EXPECT_FALSE(result);
10597
}
10698

10799
// ---------------------------------------------------------------------------
108-
// B. Vtable majority-vote logic (slow path of hasJavaThreadVtable)
100+
// B. Vtable majority-vote logic (isJavaThread slow path via hasJavaThreadVtable)
109101
//
110-
// VMThread::hasJavaThreadVtable() compares vtable entries at indices 1, 3, 5
111-
// against _java_thread_vtbl[1/3/5] and returns true when >= 2 match.
112-
// This ensures a MonitorDeflationThread (which is a JavaThread subclass but
113-
// has its own vtable) is correctly classified as a non-Java thread.
114-
//
115-
// We replicate the vote logic here as a whitebox unit test, keeping it in
116-
// sync with the comment in vmStructs.inline.h.
102+
// hasJavaThreadVtable() compares vtable entries at indices 1, 3, 5 against
103+
// _java_thread_vtbl[] captured from a known JavaThread at profiler start.
104+
// At least 2 of 3 must match — tolerating product-vs-debug vtable variation.
117105
// ---------------------------------------------------------------------------
118106

119107
namespace {
120108

121-
// Simulate the 2-of-3 majority vote performed by hasJavaThreadVtable().
122-
// Arguments:
123-
// thread_vtbl – vtable pointer array of the candidate thread
124-
// known_vtbl – _java_thread_vtbl captured from a real JavaThread
109+
// Replicates the 2-of-3 vote in hasJavaThreadVtable().
125110
static bool vtableVote(void** thread_vtbl, void** known_vtbl) {
126111
int matches = (thread_vtbl[1] == known_vtbl[1])
127112
+ (thread_vtbl[3] == known_vtbl[3])
@@ -133,74 +118,72 @@ static bool vtableVote(void** thread_vtbl, void** known_vtbl) {
133118

134119
class VtableVoteTest : public ::testing::Test {
135120
protected:
136-
// Sentinel vtable entries used as the "known JavaThread vtable".
137121
void* known[8] = {
138122
(void*)0x1000, (void*)0x1001, (void*)0x1002,
139123
(void*)0x1003, (void*)0x1004, (void*)0x1005,
140124
(void*)0x1006, (void*)0x1007
141125
};
142126

143-
// A thread vtable that exactly matches `known`.
127+
// Identical to `known` — a real JavaThread.
144128
void* same[8] = {
145129
(void*)0x1000, (void*)0x1001, (void*)0x1002,
146130
(void*)0x1003, (void*)0x1004, (void*)0x1005,
147131
(void*)0x1006, (void*)0x1007
148132
};
149133

150-
// A thread vtable that shares no entries with `known`
151-
// (simulates MonitorDeflationThread or another JVM-internal thread).
134+
// Completely different — MonitorDeflationThread or other JVM-internal thread.
152135
void* different[8] = {
153136
(void*)0x2000, (void*)0x2001, (void*)0x2002,
154137
(void*)0x2003, (void*)0x2004, (void*)0x2005,
155138
(void*)0x2006, (void*)0x2007
156139
};
157140
};
158141

159-
// Exact match (3/3) → java thread.
160142
TEST_F(VtableVoteTest, ExactMatchIsJavaThread) {
161143
EXPECT_TRUE(vtableVote(same, known));
162144
}
163145

164-
// Zero matches → not a java thread (MonitorDeflationThread case).
165146
TEST_F(VtableVoteTest, NoMatchIsNotJavaThread) {
166147
EXPECT_FALSE(vtableVote(different, known));
167148
}
168149

169-
// Exactly 2 matches → still a java thread (handles product vs. debug JVM).
150+
// 2/3 match — handles product vs. debug JVM where one entry differs.
170151
TEST_F(VtableVoteTest, TwoMatchesIsJavaThread) {
171152
void* partial[8];
172153
memcpy(partial, same, sizeof(partial));
173-
// Corrupt one of the three checked entries (index 1).
174-
partial[1] = (void*)0xDEAD;
154+
partial[1] = (void*)0xDEAD; // corrupt one of the three checked entries
175155
EXPECT_TRUE(vtableVote(partial, known));
176156
}
177157

178-
// Exactly 1 match → not a java thread.
179158
TEST_F(VtableVoteTest, OneMatchIsNotJavaThread) {
180159
void* partial[8];
181160
memcpy(partial, different, sizeof(partial));
182-
// Give it one real entry (index 3) to stay below the threshold.
183-
partial[3] = known[3];
161+
partial[3] = known[3]; // one real entry — still below threshold
184162
EXPECT_FALSE(vtableVote(partial, known));
185163
}
186164

187-
// All zeros (uninitialized _java_thread_vtbl before profiler attaches) →
188-
// a thread whose own vtable is also all-zero scores 3/3 but that cannot
189-
// happen in practice because NULL vtable entries dereference to nothing.
190-
// More practically: a non-zero vtable against an all-zero reference → 0/3.
191-
TEST_F(VtableVoteTest, UninitializedReferenceVtableGivesNoMatches) {
192-
void* zero_ref[8] = {}; // _java_thread_vtbl not yet initialized
165+
// _java_thread_vtbl not yet initialised (profiler just attached): any real
166+
// thread vtable scores 0/3 against an all-zero reference.
167+
TEST_F(VtableVoteTest, UninitializedReferenceGivesNoMatches) {
168+
void* zero_ref[8] = {};
193169
EXPECT_FALSE(vtableVote(same, zero_ref));
194170
}
195171

196172
// ---------------------------------------------------------------------------
197-
// C. setup_crash_protection gate condition
173+
// C. Crash-handler nesting depth
174+
//
175+
// ProfiledThread tracks how many crash-handler invocations are active on this
176+
// thread so recursive signals (wall-clock arriving inside a crash handler)
177+
// are capped at CRASH_HANDLER_NESTING_LIMIT.
198178
//
199-
// walkVM computes: setup_crash_protection = (vm_thread != NULL) && isJavaThread()
200-
// Verify that the gate is false for TYPE_NOT_JAVA_THREAD threads (fast path).
179+
// Profiler::crashHandlerInternal calls:
180+
// enterCrashHandler() — on entry, returns false if limit reached
181+
// exitCrashHandler() — on normal exit
182+
// resetCrashHandler() — from checkFault before longjmp to unwind all
183+
// nesting at once
201184
// ---------------------------------------------------------------------------
202185

203-
class CrashProtectionGateTest : public ::testing::Test {
186+
class CrashHandlerNestingTest : public ::testing::Test {
204187
protected:
205188
void SetUp() override {
206189
ProfiledThread::initCurrentThread();
@@ -213,43 +196,88 @@ class CrashProtectionGateTest : public ::testing::Test {
213196
}
214197

215198
ProfiledThread* _pt = nullptr;
199+
};
200+
201+
TEST_F(CrashHandlerNestingTest, InitialDepthAllowsEntry) {
202+
EXPECT_TRUE(_pt->enterCrashHandler());
203+
_pt->exitCrashHandler();
204+
}
216205

217-
// Replicate the fast-path of isJavaThread() for gating purposes.
218-
static bool fastPathIsJavaThread(ProfiledThread* pt) {
219-
if (pt == nullptr) return false;
220-
ProfiledThread::ThreadType type = pt->threadType();
221-
if (type != ProfiledThread::TYPE_UNKNOWN) {
222-
return type == ProfiledThread::TYPE_JAVA_THREAD;
223-
}
224-
return false; // TYPE_UNKNOWN → slow vtable path (not tested here)
206+
TEST_F(CrashHandlerNestingTest, ExitDecrements) {
207+
_pt->enterCrashHandler();
208+
_pt->exitCrashHandler();
209+
// After a balanced enter/exit, another enter should still succeed.
210+
EXPECT_TRUE(_pt->enterCrashHandler());
211+
_pt->exitCrashHandler();
212+
}
213+
214+
// At the nesting limit enterCrashHandler returns false to prevent runaway recursion.
215+
TEST_F(CrashHandlerNestingTest, LimitBlocksFurtherEntry) {
216+
for (u32 i = 0; i < ProfiledThread::CRASH_HANDLER_NESTING_LIMIT; i++) {
217+
EXPECT_TRUE(_pt->enterCrashHandler()) << "entry " << i << " should succeed";
225218
}
226-
};
219+
EXPECT_FALSE(_pt->enterCrashHandler()) << "entry at limit should fail";
227220

228-
// Gate is off for a JVM-internal thread (TYPE_NOT_JAVA_THREAD):
229-
// _exception_file must NOT be written.
230-
TEST_F(CrashProtectionGateTest, GateOffForNonJavaThread) {
231-
_pt->setJavaThread(false);
232-
// Simulate: vm_thread != NULL (non-null pointer means JVM thread struct exists)
233-
bool vm_thread_non_null = true;
234-
bool setup_crash_protection = vm_thread_non_null && fastPathIsJavaThread(_pt);
235-
EXPECT_FALSE(setup_crash_protection);
221+
// Clean up the nesting we opened above.
222+
for (u32 i = 0; i < ProfiledThread::CRASH_HANDLER_NESTING_LIMIT; i++) {
223+
_pt->exitCrashHandler();
224+
}
236225
}
237226

238-
// Gate is on for a real Java application thread (TYPE_JAVA_THREAD):
239-
// _exception_file IS written so crash recovery via longjmp works.
240-
TEST_F(CrashProtectionGateTest, GateOnForJavaThread) {
241-
_pt->setJavaThread(true);
242-
bool vm_thread_non_null = true;
243-
bool setup_crash_protection = vm_thread_non_null && fastPathIsJavaThread(_pt);
244-
EXPECT_TRUE(setup_crash_protection);
227+
// resetCrashHandler() is called by checkFault() before longjmp so that the
228+
// landing pad in walkVM starts with a clean nesting count.
229+
TEST_F(CrashHandlerNestingTest, ResetAllowsEntryAfterDeepNesting) {
230+
for (u32 i = 0; i < ProfiledThread::CRASH_HANDLER_NESTING_LIMIT; i++) {
231+
_pt->enterCrashHandler();
232+
}
233+
_pt->resetCrashHandler();
234+
EXPECT_TRUE(_pt->enterCrashHandler());
235+
_pt->exitCrashHandler();
236+
}
237+
238+
// exitCrashHandler is a no-op if depth is already 0 (failsafe against
239+
// unbalanced calls during error paths).
240+
TEST_F(CrashHandlerNestingTest, ExitAtZeroIsNoop) {
241+
_pt->exitCrashHandler(); // depth was 0, must not underflow
242+
EXPECT_TRUE(_pt->enterCrashHandler());
243+
_pt->exitCrashHandler();
244+
}
245+
246+
// isDeepCrashHandler returns true only when depth *exceeds* the limit.
247+
// At exactly the limit, it is false — entry is refused but "deep" is not yet set.
248+
TEST_F(CrashHandlerNestingTest, IsDeepOnlyAboveLimit) {
249+
for (u32 i = 0; i < ProfiledThread::CRASH_HANDLER_NESTING_LIMIT; i++) {
250+
_pt->enterCrashHandler();
251+
}
252+
EXPECT_FALSE(_pt->isDeepCrashHandler()); // at limit, not above it
253+
_pt->resetCrashHandler();
254+
}
255+
256+
// ---------------------------------------------------------------------------
257+
// D. Longjmp-protection state (HotspotSupport::isThreadProtectedByLongjmp)
258+
//
259+
// walkVM arms crash recovery by storing a jmp_buf* in a thread-local slot
260+
// (_jmp_ctx) before the stack walk and clearing it afterward. checkFault()
261+
// reads this slot via isThreadProtectedByLongjmp() to decide whether to
262+
// longjmp.
263+
//
264+
// HotspotSupport::initThread() initialises the slot to nullptr (so the key
265+
// is valid but the protection is off). isThreadProtectedByLongjmp() returns
266+
// true only when the slot holds a non-null jmp_buf*.
267+
// ---------------------------------------------------------------------------
268+
269+
TEST(LongjmpProtectionTest, UninitializedKeyMeansNotProtected) {
270+
// Without initThread() the pthread key may not be valid at all; either way
271+
// isThreadProtectedByLongjmp() must not crash and must return false.
272+
// (On a thread that has never called initThread the get() returns nullptr.)
273+
EXPECT_FALSE(HotspotSupport::isThreadProtectedByLongjmp());
245274
}
246275

247-
// Gate is always off when vm_thread is NULL (no JVM thread struct).
248-
TEST_F(CrashProtectionGateTest, GateOffWhenNoVmThread) {
249-
_pt->setJavaThread(true); // would be a Java thread, but vm_thread is NULL
250-
bool vm_thread_non_null = false;
251-
bool setup_crash_protection = vm_thread_non_null && fastPathIsJavaThread(_pt);
252-
EXPECT_FALSE(setup_crash_protection);
276+
TEST(LongjmpProtectionTest, AfterInitThreadKeyIsValidButNotProtected) {
277+
HotspotSupport::initThread();
278+
EXPECT_TRUE(HotspotSupport::isInitialized());
279+
// The slot is initialised to nullptr — no active walkVM on this thread.
280+
EXPECT_FALSE(HotspotSupport::isThreadProtectedByLongjmp());
253281
}
254282

255283
#endif // __linux__

0 commit comments

Comments
 (0)