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
4442class 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.
6058TEST_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.
6562TEST_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.
7368TEST_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.
7973TEST_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
119107namespace {
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().
125110static 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
134119class VtableVoteTest : public ::testing::Test {
135120protected:
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.
160142TEST_F (VtableVoteTest, ExactMatchIsJavaThread) {
161143 EXPECT_TRUE (vtableVote (same, known));
162144}
163145
164- // Zero matches → not a java thread (MonitorDeflationThread case).
165146TEST_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 .
170151TEST_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.
179158TEST_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 {
204187protected:
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