|
11 | 11 | * VMThread::isJavaThread() provides the gate. |
12 | 12 | * |
13 | 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. |
| 14 | + * 1. walkVM stores a jmp_buf* on ProfiledThread (setJmpCtx/getJmpCtx), |
| 15 | + * chaining it with whatever context was already installed so a |
| 16 | + * signal-based sampler interrupting a non-signal-based sampler's own |
| 17 | + * in-flight walkVM() call doesn't clobber the outer call's context. |
| 18 | + * 2. If a fault fires during the walk, checkFault() detects the live |
| 19 | + * context via ProfiledThread::isProtected() and calls longjmp() to |
| 20 | + * unwind through whatever context is currently installed. |
17 | 21 | * 3. ProfiledThread tracks nested crash-handler depth so recursive faults |
18 | 22 | * (e.g. wall-clock signal inside a crash handler) are capped safely. |
19 | 23 | * |
20 | 24 | * Tests cover: |
21 | 25 | * A. ProfiledThread thread-type classification (isJavaThread fast path) |
22 | 26 | * B. Vtable majority-vote logic (isJavaThread slow path) |
23 | 27 | * C. Crash-handler nesting depth (ProfiledThread crash handler state) |
24 | | - * D. Longjmp-protection state via HotspotSupport public API |
| 28 | + * D. jmp_buf chaining across nested/interrupted walkVM() calls |
25 | 29 | */ |
26 | 30 |
|
27 | 31 | #include <gtest/gtest.h> |
@@ -253,4 +257,139 @@ TEST_F(CrashHandlerNestingTest, IsDeepOnlyAboveLimit) { |
253 | 257 | _pt->resetCrashHandler(); |
254 | 258 | } |
255 | 259 |
|
| 260 | +// --------------------------------------------------------------------------- |
| 261 | +// D. jmp_buf chaining (ProfiledThread::setJmpCtx/getJmpCtx/isProtected) |
| 262 | +// |
| 263 | +// A non-signal-based sampler's walkVM() call can itself be interrupted by a |
| 264 | +// signal-based sampler, putting two walkVM() frames on the same thread's |
| 265 | +// stack. Each frame follows the same protocol: |
| 266 | +// jmp_buf* prev = prof_thread->getJmpCtx(); // save whatever was there |
| 267 | +// prof_thread->setJmpCtx(&my_ctx); // install this frame's ctx |
| 268 | +// ... walk ... |
| 269 | +// prof_thread->setJmpCtx(prev); // restore on every exit path |
| 270 | +// checkFault() always longjmps through whatever is currently installed |
| 271 | +// (thrd->getJmpCtx()), so the inner frame must never leave the outer frame's |
| 272 | +// context installed while the inner frame is doing its own protected work, |
| 273 | +// and must always hand it back — via normal completion or fault recovery — |
| 274 | +// before returning control to the outer frame. |
| 275 | +// --------------------------------------------------------------------------- |
| 276 | + |
| 277 | +class JmpCtxChainingTest : public ::testing::Test { |
| 278 | +protected: |
| 279 | + void SetUp() override { |
| 280 | + ProfiledThread::initCurrentThread(); |
| 281 | + _pt = ProfiledThread::currentSignalSafe(); |
| 282 | + ASSERT_NE(nullptr, _pt); |
| 283 | + } |
| 284 | + |
| 285 | + void TearDown() override { |
| 286 | + ProfiledThread::release(); |
| 287 | + } |
| 288 | + |
| 289 | + ProfiledThread* _pt = nullptr; |
| 290 | +}; |
| 291 | + |
| 292 | +TEST_F(JmpCtxChainingTest, InitiallyUnprotected) { |
| 293 | + EXPECT_FALSE(_pt->isProtected()); |
| 294 | + EXPECT_EQ(nullptr, _pt->getJmpCtx()); |
| 295 | +} |
| 296 | + |
| 297 | +TEST_F(JmpCtxChainingTest, SetAndGetRoundTrip) { |
| 298 | + jmp_buf ctx; |
| 299 | + _pt->setJmpCtx(&ctx); |
| 300 | + EXPECT_TRUE(_pt->isProtected()); |
| 301 | + EXPECT_EQ(&ctx, _pt->getJmpCtx()); |
| 302 | +} |
| 303 | + |
| 304 | +// Replicates a single walkVM() call's save/install/restore around its body. |
| 305 | +TEST_F(JmpCtxChainingTest, SingleFrameRestoresPreviousOnExit) { |
| 306 | + jmp_buf outer; |
| 307 | + jmp_buf* prev = _pt->getJmpCtx(); // nullptr: no enclosing walkVM() call |
| 308 | + ASSERT_EQ(nullptr, prev); |
| 309 | + |
| 310 | + _pt->setJmpCtx(&outer); |
| 311 | + EXPECT_EQ(&outer, _pt->getJmpCtx()); |
| 312 | + |
| 313 | + // Simulate walkVM()'s `done:` path. |
| 314 | + _pt->setJmpCtx(prev); |
| 315 | + EXPECT_EQ(nullptr, _pt->getJmpCtx()); |
| 316 | + EXPECT_FALSE(_pt->isProtected()); |
| 317 | +} |
| 318 | + |
| 319 | +// Replicates two nested walkVM() calls: a signal-based sampler interrupting a |
| 320 | +// non-signal-based sampler's own in-flight walkVM(). The inner call must |
| 321 | +// chain off the outer's jmp_buf*, install its own, and hand the outer's back |
| 322 | +// on its way out — leaving the outer frame's context exactly as it left it. |
| 323 | +TEST_F(JmpCtxChainingTest, NestedFramesChainAndUnwindInOrder) { |
| 324 | + jmp_buf outer_ctx; |
| 325 | + jmp_buf* outer_prev = _pt->getJmpCtx(); |
| 326 | + ASSERT_EQ(nullptr, outer_prev); |
| 327 | + _pt->setJmpCtx(&outer_ctx); |
| 328 | + EXPECT_EQ(&outer_ctx, _pt->getJmpCtx()); |
| 329 | + |
| 330 | + { |
| 331 | + // Inner walkVM() call, as if a signal fired while the outer one was |
| 332 | + // mid-walk. |
| 333 | + jmp_buf inner_ctx; |
| 334 | + jmp_buf* inner_prev = _pt->getJmpCtx(); |
| 335 | + EXPECT_EQ(&outer_ctx, inner_prev); // chained off the outer frame |
| 336 | + |
| 337 | + _pt->setJmpCtx(&inner_ctx); |
| 338 | + EXPECT_EQ(&inner_ctx, _pt->getJmpCtx()); |
| 339 | + |
| 340 | + // Inner call completes via its own `done:` path. |
| 341 | + _pt->setJmpCtx(inner_prev); |
| 342 | + } |
| 343 | + |
| 344 | + // The outer frame's context must be untouched by the inner call. |
| 345 | + EXPECT_EQ(&outer_ctx, _pt->getJmpCtx()); |
| 346 | + |
| 347 | + _pt->setJmpCtx(outer_prev); |
| 348 | + EXPECT_EQ(nullptr, _pt->getJmpCtx()); |
| 349 | +} |
| 350 | + |
| 351 | +// End-to-end with real setjmp/longjmp: a fault inside the inner frame must |
| 352 | +// land in the inner frame's own recovery branch — checkFault() always |
| 353 | +// longjmps through whatever is currently installed — and once the inner |
| 354 | +// frame has recovered and restored the outer's context, the outer frame must |
| 355 | +// be left exactly as it was, never having been unwound itself. |
| 356 | +TEST_F(JmpCtxChainingTest, FaultInInnerFrameDoesNotDisturbOuterFrame) { |
| 357 | + jmp_buf outer_ctx; |
| 358 | + jmp_buf* outer_prev = _pt->getJmpCtx(); |
| 359 | + int outer_landed = 0; |
| 360 | + int inner_landed = 0; |
| 361 | + |
| 362 | + if (setjmp(outer_ctx) != 0) { |
| 363 | + outer_landed++; |
| 364 | + } else { |
| 365 | + _pt->setJmpCtx(&outer_ctx); |
| 366 | + |
| 367 | + // --- inner "walkVM" call, interrupted mid-flight by a fault --- |
| 368 | + jmp_buf inner_ctx; |
| 369 | + jmp_buf* inner_prev = _pt->getJmpCtx(); |
| 370 | + ASSERT_EQ(&outer_ctx, inner_prev); |
| 371 | + |
| 372 | + if (setjmp(inner_ctx) != 0) { |
| 373 | + inner_landed++; |
| 374 | + _pt->setJmpCtx(inner_prev); |
| 375 | + } else { |
| 376 | + _pt->setJmpCtx(&inner_ctx); |
| 377 | + // Simulate checkFault(): longjmp through whatever is currently |
| 378 | + // installed — this must hit the inner frame, not the outer. |
| 379 | + longjmp(*_pt->getJmpCtx(), 1); |
| 380 | + FAIL() << "unreachable: longjmp does not return"; |
| 381 | + } |
| 382 | + // --- inner call has returned normally after recovering --- |
| 383 | + |
| 384 | + EXPECT_EQ(&outer_ctx, _pt->getJmpCtx()) |
| 385 | + << "outer frame's context must survive the inner frame's fault"; |
| 386 | + |
| 387 | + _pt->setJmpCtx(outer_prev); |
| 388 | + } |
| 389 | + |
| 390 | + EXPECT_EQ(1, inner_landed); |
| 391 | + EXPECT_EQ(0, outer_landed) << "the fault must not have unwound past the inner frame"; |
| 392 | + EXPECT_FALSE(_pt->isProtected()); |
| 393 | +} |
| 394 | + |
256 | 395 | #endif // __linux__ |
0 commit comments