From 628d42f52444f51746c2f0497358fa6df12d9c72 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 24 Jul 2026 18:12:06 +0000 Subject: [PATCH 01/51] v0 --- ddprof-lib/src/main/cpp/counters.h | 2 +- .../src/main/cpp/hotspot/hotspotSupport.cpp | 16 --- .../src/main/cpp/hotspot/hotspotSupport.h | 1 - ddprof-lib/src/main/cpp/profiler.cpp | 103 ++++++++++++++---- ddprof-lib/src/main/cpp/profiler.h | 2 + ddprof-lib/src/main/cpp/vmEntry.cpp | 3 + ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 97 ++++++++++++++++- .../test/cpp/hotspot_crash_protection_ut.cpp | 6 +- 8 files changed, 185 insertions(+), 45 deletions(-) diff --git a/ddprof-lib/src/main/cpp/counters.h b/ddprof-lib/src/main/cpp/counters.h index 623e2c95a5..758931fcef 100644 --- a/ddprof-lib/src/main/cpp/counters.h +++ b/ddprof-lib/src/main/cpp/counters.h @@ -133,7 +133,7 @@ X(SAMPLES_DROPPED_THREAD_LOCAL, "samples_dropped_thread_local") \ X(SAFECOPY_FAILED, "safecopy_failed") \ X(SAFEFETCH_FAILED, "safefetch_failed") \ - X(WALKVM_LONGJMP_RECOVERED, "walkvm_longjmp_recovered") \ + X(STACKWALK_LONGJMP_RECOVERED, "stackwalk_longjmp_recovered") \ DD_COUNTER_TABLE_FAULT_INJECTION(X) \ DD_COUNTER_TABLE_FI_DEBUG(X) \ DD_COUNTER_TABLE_DEBUG(X) diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index 9e4ad72834..945b00cd94 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -978,22 +978,6 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex return depth; } -void HotspotSupport::checkFault(ProfiledThread* thrd) { - // Should not get to here (?) - if (thrd == nullptr) { - return; - } - - // Check if longjmp is setup for this thread - if (!thrd->isProtected()) { - return; - } - - thrd->resetCrashHandler(); - Counters::increment(WALKVM_LONGJMP_RECOVERED); - longjmp(*thrd->getJmpCtx(), 1); -} - int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, int max_depth, StackContext *java_ctx, bool *truncated) { diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h index e905d55c4a..6aed4ff00d 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h @@ -36,7 +36,6 @@ class HotspotSupport { public: static void initClassloaderInfo(JNIEnv* jni); - static void checkFault(ProfiledThread* thrd = nullptr); static int walkJavaStack(StackWalkRequest& request); static inline bool canUnwind(const StackFrame& frame, const void*& pc) { return HotspotStackFrame::unwindAtomicStub(frame, pc); diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 51769089a1..199c3388c7 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -659,30 +659,75 @@ bool Profiler::recordSample(void *ucontext, u64 counter, int tid, #endif // COUNTERS ASGCT_CallFrame *frames = _calltrace_buffer[lock_index]->_asgct_frames; - int num_frames = 0; - + // Read again after the setjmp landing below, so it must be volatile: a + // longjmp out of the unwind leaves non-volatile locals indeterminate. + volatile int num_frames = 0; StackContext java_ctx = {0}; - ASGCT_CallFrame *native_stop = frames + num_frames; - num_frames += getNativeTrace(ucontext, native_stop, event_type, tid, - &java_ctx, &truncated, lock_index); - assert(num_frames >= 0); - - int max_remaining = _max_stack_depth - num_frames; - if (max_remaining > 0) { - StackWalkRequest request = {event_type, lock_index, ucontext, frames + num_frames, max_remaining, &java_ctx, &truncated}; - num_frames += JVMSupport::walkJavaStack(request); + + // Establish setjmp/longjmp crash protection around the unwind. The native + // walkers (walkFP/walkDwarf) protect their pointer loads with SafeAccess + // safefetch, but the surrounding metadata reads (isJitCode, findFrameDesc, + // findLibraryByAddress, AGCT in getJavaTraceAsync, frame.link, ...) are raw + // dereferences of signal-supplied pc/sp/fp. Without an active jmp_buf a + // fault there is unrecoverable: crashHandlerInternal -> checkFault() finds + // isProtected()==false and chains to the JVM handler, crashing the process. + // walkVM installs its own inner jmp_buf and chains back to whatever we set + // here, so nesting is safe. When there is no ProfiledThread we have nowhere + // to publish a jmp_buf, so we skip the unwind entirely rather than risk an + // unrecoverable fault on a raw dereference. + ProfiledThread *walk_thread = ProfiledThread::current(); + if (walk_thread == nullptr) { + num_frames += makeFrame(frames + num_frames, BCI_ERROR, "no_ProfiledThread"); + truncated = true; + } else { + jmp_buf unwind_ctx; + jmp_buf *prev_jmp_buf = walk_thread->getJmpCtx(); + + // setjmp() must be evaluated as a standalone expression/controlling + // expression (C11 7.13.1.1), hence the explicit jmp_rc. + int jmp_rc = setjmp(unwind_ctx); + if (jmp_rc != 0) { + // A fault during unwinding longjmp'd back here (via checkFault). The + // longjmp bypassed segvHandler's SignalHandlerScope destructor, so + // compensate, restore the previous jmp_buf chain, and record the + // partial trace with an error marker instead of crashing. + SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); + walk_thread->setJmpCtx(prev_jmp_buf); + truncated = true; + if (num_frames < _max_stack_depth) { + num_frames += makeFrame(frames + num_frames, BCI_ERROR, "break_unwind_fault"); + } + } else { + walk_thread->setJmpCtx(&unwind_ctx); + + // truncated_local is never read after a longjmp landing (only on the + // clean path below), so it need not be volatile; the outer `truncated` + // stays false on the recovery path. + bool truncated_local = false; + ASGCT_CallFrame *native_stop = frames + num_frames; + num_frames += getNativeTrace(ucontext, native_stop, event_type, tid, + &java_ctx, &truncated_local, lock_index); + assert(num_frames >= 0); + + int max_remaining = _max_stack_depth - num_frames; + if (max_remaining > 0) { + StackWalkRequest request = {event_type, lock_index, ucontext, frames + num_frames, max_remaining, &java_ctx, &truncated_local}; + num_frames += JVMSupport::walkJavaStack(request); + } + assert(num_frames >= 0); + + walk_thread->setJmpCtx(prev_jmp_buf); + truncated = truncated_local; + } } - - assert(num_frames >= 0); if (num_frames == 0) { num_frames += makeFrame(frames + num_frames, BCI_ERROR, "no_Java_frame"); } call_trace_id = _call_trace_storage.put(num_frames, frames, truncated, counter); - ProfiledThread *thread = ProfiledThread::current(); - if (thread != nullptr) { - thread->recordCallTraceId(call_trace_id); + if (walk_thread != nullptr) { + walk_thread->recordCallTraceId(call_trace_id); } #ifdef COUNTERS u64 duration = TSC::ticks() - startTime; @@ -1011,14 +1056,14 @@ int Profiler::crashHandlerInternal(int signo, siginfo_t *siginfo, void *ucontext return 1; // handled } + // Profiler::checkFault has its own check if we're in a protected stack walk. + // If the fault is from our protected walk, it will longjmp and never return. + // If it returns, the fault wasn't from our code. + Profiler::checkFault(thrd); + if (VM::isHotspot()) { // the following checks require vmstructs and therefore HotSpot - // HotspotSupport::checkFault has its own check if we're in a protected stack walk. - // If the fault is from our protected walk, it will longjmp and never return. - // If it returns, the fault wasn't from our code. - HotspotSupport::checkFault(thrd); - // Workaround for JDK-8313796 if needed. Setting cstack=dwarf also helps if (_need_JDK_8313796_workaround && VMStructs::isInterpretedFrameValidFunc((const void *)pc) && @@ -1981,3 +2026,19 @@ int Profiler::status(char* status, int max_len) { _wall_engine != nullptr ? _wall_engine->name() : "None", _alloc_engine != nullptr ? _alloc_engine->name() : "None"); } + +void Profiler::checkFault(ProfiledThread* thrd) { + // Should not get to here (?) + if (thrd == nullptr) { + return; + } + + // Check if longjmp is setup for this thread + if (!thrd->isProtected()) { + return; + } + + thrd->resetCrashHandler(); + Counters::increment(STACKWALK_LONGJMP_RECOVERED); + longjmp(*thrd->getJmpCtx(), 1); +} diff --git a/ddprof-lib/src/main/cpp/profiler.h b/ddprof-lib/src/main/cpp/profiler.h index 946c4c1e6d..dfe26a486d 100644 --- a/ddprof-lib/src/main/cpp/profiler.h +++ b/ddprof-lib/src/main/cpp/profiler.h @@ -236,6 +236,8 @@ class alignas(alignof(SpinLock)) Profiler { return _instance; } + static void checkFault(ProfiledThread* thrd = nullptr); + // Resolve names of native (non-Java) threads from /proc. Idempotent and // allocation-light (no-op for already-named tids), so it is safe to call // periodically from the Libraries refresher thread to capture transient diff --git a/ddprof-lib/src/main/cpp/vmEntry.cpp b/ddprof-lib/src/main/cpp/vmEntry.cpp index 670c54af6c..f6b6946ad7 100644 --- a/ddprof-lib/src/main/cpp/vmEntry.cpp +++ b/ddprof-lib/src/main/cpp/vmEntry.cpp @@ -217,6 +217,9 @@ CodeCache* VM::openJvmLibrary() { lib = isOpenJ9() ? libraries->findJvmLibrary("libj9vm") : libraries->findLibraryByAddress((const void *)_asyncGetCallTrace); + // The library must have been loaded. Otherwise, we cannot get to here due + // to JVM initialization + assert(lib != nullptr && "JVM library must be loaded"); __atomic_store_n(&_libjvm, lib, __ATOMIC_RELEASE); return lib; } diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index abb81b8ed3..31d56f43d3 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -10,11 +10,12 @@ #include #include +#include "counters.h" #include "faultInjection.h" #include "safeAccess.h" #include "os.h" #include "threadLocalData.h" -#include "hotspot/hotspotSupport.h" +#include "profiler.h" #include "../../main/cpp/gtest_crash_handler.h" static constexpr char FAULT_INJECTION_TEST_NAME[] = "FaultInjectionTest"; @@ -60,7 +61,7 @@ static void fi_signal_wrapper(int signo, siginfo_t* siginfo, void* context) { if (SafeAccess::handle_safefetch(signo, context)) { return; // safefetch load recovered; PC already rewritten to _cont. } - HotspotSupport::checkFault(ProfiledThread::current()); // longjmp if protected + Profiler::checkFault(ProfiledThread::current()); // longjmp if protected // Not protected and not a safefetch fault — real crash. if (signo == SIGBUS && orig_busHandler != nullptr) { orig_busHandler(signo, siginfo, context); @@ -167,7 +168,9 @@ TEST_F(FaultInjectionTest, WalkVmSetjmpRecoversFromInjectedFault) { for (int i = 0; i < 5000 && faults == 0; i++) { // Raw deref of the (possibly poisoned) base — mirrors walkVM's raw reads. uintptr_t v = *(uintptr_t*)INJECT_FAULT_ADDRESS_LIKELY(base); - (void)v; + // Optimization barrier: tell the compiler `v` is read/write and clobber memory to prevent + // reordering/optimizing away the load. + asm volatile("" : "+r"(v) : : "memory"); reads++; } t->setJmpCtx(nullptr); @@ -180,4 +183,92 @@ TEST_F(FaultInjectionTest, WalkVmSetjmpRecoversFromInjectedFault) { SUCCEED(); } +// (c3) recordSample's outer setjmp region (PROF-15447): Profiler::recordSample() +// wraps the native/Java unwind in its own jmp_buf, chaining off whatever +// context a caller may already have installed, so a fault in the +// *unprotected* metadata reads surrounding walkVM/walkFP/walkDwarf (isJitCode, +// findFrameDesc, findLibraryByAddress, AGCT in getJavaTraceAsync, frame.link, +// ...) recovers to a partial trace instead of crashing — and, critically, +// restores the caller's jmp_buf chain on both the clean and the recovery path. +// +// recordSample() itself needs a live JVM (ASGCT, VMStructs, an allocated +// _calltrace_buffer) unavailable in this gtest binary, so — following the +// same "replicate the protocol" approach used elsewhere in this suite for +// JVM-dependent code — this test reproduces its exact save/install/setjmp/ +// longjmp/restore sequence verbatim, including the `volatile int num_frames` +// accumulator that must survive the longjmp landing, and drives it with the +// same probabilistic fault injection as WalkVmSetjmpRecoversFromInjectedFault. +// Unlike that test, this one also (a) starts from an already-installed +// "grandparent" jmp_buf to verify nesting/chain-restore, matching how +// recordSample can itself run nested under another sampler's protection, and +// (b) asserts STACKWALK_LONGJMP_RECOVERED actually increments, confirming the +// real Profiler::checkFault() (wired in via fi_signal_wrapper) did the +// recovery rather than some other signal-handling path. +TEST_F(FaultInjectionTest, RecordSampleOuterSetjmpRecoversAndRestoresChain) { + ProfiledThread* t = ProfiledThread::current(); + ASSERT_NE(t, nullptr); + + // Simulate a pre-existing outer protection context, as recordSample must + // support when nested inside another sampler's own protected region. + jmp_buf grandparent_ctx; + t->setJmpCtx(&grandparent_ctx); + + uintptr_t real_slot = 0; // a valid, readable "metadata" slot + uintptr_t base = (uintptr_t)&real_slot; + long long recovered_before = Counters::getCounter(STACKWALK_LONGJMP_RECOVERED); + + // Read again after the setjmp landing below, so it must be volatile — mirrors + // recordSample's own num_frames. + volatile int num_frames = 0; + // Frames "collected" before the fault, e.g. by a getNativeTrace() call that + // partially succeeded before walkJavaStack() faulted. Seeding a non-zero + // value here — mutated between setjmp() and the longjmp below — is what + // actually exercises the volatile qualifier: a non-volatile local would be + // indeterminate after the jump, so the post-recovery checks below would not + // reliably see it. Without this seed, num_frames would still read 0 whether + // or not it were volatile, and the test would prove nothing about volatile. + constexpr int kSeededFrames = 3; + + jmp_buf unwind_ctx; + jmp_buf* prev_jmp_buf = t->getJmpCtx(); + ASSERT_EQ(&grandparent_ctx, prev_jmp_buf); + + t->setFiRng(0xC0FFEEC0FFEEC0FFULL); + + int jmp_rc = setjmp(unwind_ctx); + if (jmp_rc != 0) { + // Landed here via the real Profiler::checkFault() -> longjmp, triggered by + // an actual injected fault below. Mirrors recordSample's + // `if (num_frames < _max_stack_depth) { num_frames += makeFrame(...); }`: + // the recovery marker is appended to whatever partial progress survived + // the jump, never resets it. + t->setJmpCtx(prev_jmp_buf); + num_frames += 1; // stand-in for makeFrame(..., "break_unwind_fault") + } else { + t->setJmpCtx(&unwind_ctx); + num_frames = kSeededFrames; // mutated before the fault; must survive the longjmp + + // Force at least one fire deterministically, then let the tier drive the rest. + for (int i = 0; i < 5000 && num_frames == kSeededFrames; i++) { + // Raw deref of the (possibly poisoned) base — mirrors the unprotected + // metadata reads recordSample's outer setjmp now guards. + uintptr_t v = *(uintptr_t*)INJECT_FAULT_ADDRESS_LIKELY(base); + asm volatile("" : "+r"(v) : : "memory"); + } + t->setJmpCtx(prev_jmp_buf); + } + + EXPECT_EQ(&grandparent_ctx, t->getJmpCtx()) + << "must restore the caller's jmp_buf chain on both the clean and the " + "recovery path, never leave it cleared or pointing at unwind_ctx"; + EXPECT_EQ(kSeededFrames + 1, num_frames) + << "the seeded pre-fault value must survive the longjmp landing and the " + "recovery marker must append to it, not overwrite it"; + EXPECT_GT(Counters::getCounter(STACKWALK_LONGJMP_RECOVERED), recovered_before) + << "checkFault() must have run and incremented the shared recovery counter"; + + t->setJmpCtx(nullptr); // leave the thread unprotected for subsequent tests +} + + #endif // __FAULT_INJECTION__ diff --git a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp index 1f434aa4b4..385d9abedb 100644 --- a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp +++ b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp @@ -29,7 +29,7 @@ #include #include "threadLocalData.h" -#include "hotspot/hotspotSupport.h" +#include "profiler.h" #include "jvmThread.h" #include "safeAccess.h" @@ -325,7 +325,7 @@ TEST_F(JmpCtxChainingTest, FaultInInnerFrameDoesNotDisturbOuterFrame) { } // --------------------------------------------------------------------------- -// D. HotspotSupport::checkFault() guard clauses +// D. Profiler::checkFault() guard clauses // // This gtest binary has no live JVM attached, so JVMThread is not initialized // and the longjmp path can't be exercised end-to-end here. @@ -334,7 +334,7 @@ TEST_F(JmpCtxChainingTest, FaultInInnerFrameDoesNotDisturbOuterFrame) { // --------------------------------------------------------------------------- TEST(CheckFaultGuardTest, NullThreadIsNoop) { - HotspotSupport::checkFault(nullptr); // must not crash + Profiler::checkFault(nullptr); // must not crash } // --------------------------------------------------------------------------- From a300d48e0022bc4f871a4de9d29181cdac2aa751 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 24 Jul 2026 19:53:25 +0000 Subject: [PATCH 02/51] v1 --- .../src/main/cpp/hotspot/hotspotSupport.cpp | 6 +++--- ddprof-lib/src/main/cpp/profiler.cpp | 18 ++++++++++-------- ddprof-lib/src/main/cpp/profiler.h | 3 +-- ddprof-lib/src/main/cpp/threadLocalData.h | 6 +++--- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index 945b00cd94..6404d22be8 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -248,18 +248,18 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex int bcp_offset = InterpreterFrame::bcp_offset(); - jmp_buf crash_protection_ctx; + sigjmp_buf crash_protection_ctx; // Chaining jmp_buf // A non-signal-based-sampler can be interrupted by signal based sampler, // then we end up with multiple HotspotSupport::walkVM() calls on stack, // each one sets up jmp_buf, they need to be chained to jump back to // correct location. - jmp_buf* prev_jmp_buf = prof_thread->getJmpCtx(); + sigjmp_buf* prev_jmp_buf = prof_thread->getJmpCtx(); // Should be preserved across setjmp/longjmp volatile int depth = 0; int actual_max_depth = truncated ? max_depth + 1 : max_depth; - if (setjmp(crash_protection_ctx) != 0) { + if (sigsetjmp(crash_protection_ctx, 1) != 0) { // checkFault() does a longjmp from inside segvHandler, bypassing // segvHandler's SignalHandlerScope destructor. Compensate. SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 199c3388c7..17c6b2fc67 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -678,14 +678,16 @@ bool Profiler::recordSample(void *ucontext, u64 counter, int tid, ProfiledThread *walk_thread = ProfiledThread::current(); if (walk_thread == nullptr) { num_frames += makeFrame(frames + num_frames, BCI_ERROR, "no_ProfiledThread"); + Counters::increment(SAMPLES_DROPPED_THREAD_LOCAL); truncated = true; } else { - jmp_buf unwind_ctx; - jmp_buf *prev_jmp_buf = walk_thread->getJmpCtx(); + sigjmp_buf unwind_ctx; + sigjmp_buf *prev_jmp_buf = walk_thread->getJmpCtx(); - // setjmp() must be evaluated as a standalone expression/controlling - // expression (C11 7.13.1.1), hence the explicit jmp_rc. - int jmp_rc = setjmp(unwind_ctx); + int jmp_rc = 0; + if (!VM::isOpenJ9()) { + jmp_rc = sigsetjmp(unwind_ctx, 1); + } if (jmp_rc != 0) { // A fault during unwinding longjmp'd back here (via checkFault). The // longjmp bypassed segvHandler's SignalHandlerScope destructor, so @@ -721,7 +723,7 @@ bool Profiler::recordSample(void *ucontext, u64 counter, int tid, } } if (num_frames == 0) { - num_frames += makeFrame(frames + num_frames, BCI_ERROR, "no_Java_frame"); + num_frames += makeFrame(frames + num_frames, BCI_ERROR, "no_Java_frame"); } call_trace_id = @@ -2029,7 +2031,7 @@ int Profiler::status(char* status, int max_len) { void Profiler::checkFault(ProfiledThread* thrd) { // Should not get to here (?) - if (thrd == nullptr) { + if (thrd == nullptr || VM::isOpenJ9()) { return; } @@ -2040,5 +2042,5 @@ void Profiler::checkFault(ProfiledThread* thrd) { thrd->resetCrashHandler(); Counters::increment(STACKWALK_LONGJMP_RECOVERED); - longjmp(*thrd->getJmpCtx(), 1); + siglongjmp(*thrd->getJmpCtx(), 1); } diff --git a/ddprof-lib/src/main/cpp/profiler.h b/ddprof-lib/src/main/cpp/profiler.h index dfe26a486d..2afc591a0d 100644 --- a/ddprof-lib/src/main/cpp/profiler.h +++ b/ddprof-lib/src/main/cpp/profiler.h @@ -236,8 +236,6 @@ class alignas(alignof(SpinLock)) Profiler { return _instance; } - static void checkFault(ProfiledThread* thrd = nullptr); - // Resolve names of native (non-Java) threads from /proc. Idempotent and // allocation-light (no-op for already-named tids), so it is safe to call // periodically from the Libraries refresher thread to capture transient @@ -465,6 +463,7 @@ class alignas(alignof(SpinLock)) Profiler { static void segvHandler(int signo, siginfo_t *siginfo, void *ucontext); static void busHandler(int signo, siginfo_t *siginfo, void *ucontext); static void setupSignalHandlers(); + static void checkFault(ProfiledThread* thrd); static int registerThread(int tid); static void unregisterThread(int tid); diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index 3f143efa62..4a8da4a50a 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -63,7 +63,7 @@ class ProfiledThread : public ThreadLocalData { // SEGV-handler context on the same thread; atomic makes the publish/observe // ordering explicit instead of relying on plain load/store, matching how // _crash_depth is hardened below. - std::atomic _jmp_buf; + std::atomic _jmp_buf; u64 _pc; u64 _sp; @@ -238,11 +238,11 @@ class ProfiledThread : public ThreadLocalData { return __atomic_load_n(&_crash_depth, __ATOMIC_RELAXED) > CRASH_HANDLER_NESTING_LIMIT; } - inline void setJmpCtx(jmp_buf* buf) { + inline void setJmpCtx(sigjmp_buf* buf) { _jmp_buf = buf; } - inline jmp_buf* getJmpCtx() const { + inline sigjmp_buf* getJmpCtx() const { return _jmp_buf; } From 30511b904c3004f51241b0bc35e11794feb3fca1 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sat, 25 Jul 2026 22:03:50 +0000 Subject: [PATCH 03/51] v2 --- ddprof-lib/src/main/cpp/profiler.cpp | 34 ++++++++++++++----- ddprof-lib/src/main/cpp/profiler.h | 2 +- .../test/cpp/hotspot_crash_protection_ut.cpp | 2 +- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 17c6b2fc67..39e811cd81 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -1061,7 +1061,7 @@ int Profiler::crashHandlerInternal(int signo, siginfo_t *siginfo, void *ucontext // Profiler::checkFault has its own check if we're in a protected stack walk. // If the fault is from our protected walk, it will longjmp and never return. // If it returns, the fault wasn't from our code. - Profiler::checkFault(thrd); + Profiler::checkFault(thrd, siginfo, ucontext); if (VM::isHotspot()) { // the following checks require vmstructs and therefore HotSpot @@ -1083,6 +1083,9 @@ int Profiler::crashHandlerInternal(int signo, siginfo_t *siginfo, void *ucontext return 0; // not handled, safe to chain } +static const void* profiler_min_address = nullptr; +static const void* profiler_max_address = nullptr; + void Profiler::setupSignalHandlers() { // Do not re-run the signal setup (run only when VM has not been loaded yet) if (__sync_bool_compare_and_swap(&_signals_initialized, false, true)) { @@ -1109,7 +1112,16 @@ void Profiler::setupSignalHandlers() { // Patch sigaction GOT in libraries with broken signal handlers (already loaded) LibraryPatcher::patch_sigaction(); } -#ifdef __FAULT_INJECTION__ + + // Get address range of java profiler library + Libraries* libs = Libraries::instance(); + CodeCache* prof_lib = libs->findLibraryByName("libjavaProfiler"); + assert(prof_lib != nullptr); + profiler_min_address = prof_lib->minAddress(); + profiler_max_address = prof_lib->maxAddress(); + assert(profiler_min_address != nullptr && profiler_max_address != nullptr); + + #ifdef __FAULT_INJECTION__ // Reserve the PROT_NONE guard region used to poison memory-access sites. // Done here (off the signal path) once handlers are installed. faultinj::init(); @@ -2029,15 +2041,21 @@ int Profiler::status(char* status, int max_len) { _alloc_engine != nullptr ? _alloc_engine->name() : "None"); } -void Profiler::checkFault(ProfiledThread* thrd) { - // Should not get to here (?) - if (thrd == nullptr || VM::isOpenJ9()) { +void Profiler::checkFault(ProfiledThread* thrd, siginfo_t *siginfo, void *ucontext) { + // Check if longjmp is setup for this thread + if (thrd == nullptr || !thrd->isProtected()) { return; } - // Check if longjmp is setup for this thread - if (!thrd->isProtected()) { - return; + // Check if the fault is originated from java profiler + const uintptr_t pc = StackFrame(ucontext).pc(); + const uintptr_t min = (uintptr_t)profiler_min_address; + const uintptr_t max = (uintptr_t)profiler_max_address; + + // If the profiler address range is not initialized (e.g. unit tests), fall back + // to recovering unconditionally when a protection context is installed. + if ((min != 0 && max != 0) && (pc < min || pc >= max)) { + return; } thrd->resetCrashHandler(); diff --git a/ddprof-lib/src/main/cpp/profiler.h b/ddprof-lib/src/main/cpp/profiler.h index 2afc591a0d..c4d2bf067a 100644 --- a/ddprof-lib/src/main/cpp/profiler.h +++ b/ddprof-lib/src/main/cpp/profiler.h @@ -463,7 +463,7 @@ class alignas(alignof(SpinLock)) Profiler { static void segvHandler(int signo, siginfo_t *siginfo, void *ucontext); static void busHandler(int signo, siginfo_t *siginfo, void *ucontext); static void setupSignalHandlers(); - static void checkFault(ProfiledThread* thrd); + static void checkFault(ProfiledThread* thrd, siginfo_t *siginfo, void *ucontext); static int registerThread(int tid); static void unregisterThread(int tid); diff --git a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp index 385d9abedb..5a8c03d67e 100644 --- a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp +++ b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp @@ -334,7 +334,7 @@ TEST_F(JmpCtxChainingTest, FaultInInnerFrameDoesNotDisturbOuterFrame) { // --------------------------------------------------------------------------- TEST(CheckFaultGuardTest, NullThreadIsNoop) { - Profiler::checkFault(nullptr); // must not crash + Profiler::checkFault(nullptr, nullptr, nullptr); // must not crash } // --------------------------------------------------------------------------- From bbed59109ee7ddcaba127ef7d3188ff740ee2e7d Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sat, 25 Jul 2026 22:21:14 +0000 Subject: [PATCH 04/51] v3 --- ddprof-lib/src/main/cpp/profiler.cpp | 5 +---- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 39e811cd81..9494c8a15a 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -684,10 +684,7 @@ bool Profiler::recordSample(void *ucontext, u64 counter, int tid, sigjmp_buf unwind_ctx; sigjmp_buf *prev_jmp_buf = walk_thread->getJmpCtx(); - int jmp_rc = 0; - if (!VM::isOpenJ9()) { - jmp_rc = sigsetjmp(unwind_ctx, 1); - } + int jmp_rc = jmp_rc = sigsetjmp(unwind_ctx, 1); if (jmp_rc != 0) { // A fault during unwinding longjmp'd back here (via checkFault). The // longjmp bypassed segvHandler's SignalHandlerScope destructor, so diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index 31d56f43d3..9fccd78d71 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -61,7 +61,7 @@ static void fi_signal_wrapper(int signo, siginfo_t* siginfo, void* context) { if (SafeAccess::handle_safefetch(signo, context)) { return; // safefetch load recovered; PC already rewritten to _cont. } - Profiler::checkFault(ProfiledThread::current()); // longjmp if protected + Profiler::checkFault(ProfiledThread::current(), siginfo, context); // longjmp if protected // Not protected and not a safefetch fault — real crash. if (signo == SIGBUS && orig_busHandler != nullptr) { orig_busHandler(signo, siginfo, context); From 9ba087e270727266127c105d7b5177b9eebc92cd Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 24 Jul 2026 21:56:17 +0000 Subject: [PATCH 05/51] Fix --- ddprof-lib/src/main/cpp/counters.h | 6 +++--- ddprof-lib/src/main/cpp/faultInjection.cpp | 2 +- ddprof-lib/src/main/cpp/faultInjection.h | 2 +- ddprof-lib/src/main/cpp/guards.h | 8 ++++---- ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp | 14 +++++++------- ddprof-lib/src/main/cpp/hotspot/vmStructs.h | 4 ++-- ddprof-lib/src/main/cpp/profiler.cpp | 12 ++++++------ ddprof-lib/src/main/cpp/safeAccess.h | 4 ++-- ddprof-lib/src/main/cpp/threadLocalData.h | 2 +- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 4 ++-- .../src/test/cpp/hotspot_crash_protection_ut.cpp | 12 ++++++------ 11 files changed, 35 insertions(+), 35 deletions(-) diff --git a/ddprof-lib/src/main/cpp/counters.h b/ddprof-lib/src/main/cpp/counters.h index 758931fcef..deb6e40d9a 100644 --- a/ddprof-lib/src/main/cpp/counters.h +++ b/ddprof-lib/src/main/cpp/counters.h @@ -149,10 +149,10 @@ #endif // Fault-injection + debug only: faults injected while the current thread was -// NOT inside a walkVM longjmp-protected region. Such a site relies solely on +// NOT inside a walkVM siglongjmp-protected region. Such a site relies solely on // safefetch (or would genuinely crash if the poisoned pointer is raw-dereferenced // outside any recovery), so a non-zero value flags injection sites that are not -// covered by longjmp protection. Compiled in only when both __FAULT_INJECTION__ +// covered by siglongjmp protection. Compiled in only when both __FAULT_INJECTION__ // and DEBUG are defined. #if defined(__FAULT_INJECTION__) && defined(DEBUG) #define DD_COUNTER_TABLE_FI_DEBUG(X) \ @@ -162,7 +162,7 @@ #endif // Debug-only counters: SafeAccess reads/copies issued while the thread is -// already inside a walkVM longjmp-protected region (redundant safefetch +// already inside a walkVM siglongjmp-protected region (redundant safefetch // overhead). Not compiled into release builds at all, so they occupy no enum // slot and add no storage there. #ifdef DEBUG diff --git a/ddprof-lib/src/main/cpp/faultInjection.cpp b/ddprof-lib/src/main/cpp/faultInjection.cpp index c42298ec2d..d61be77b68 100644 --- a/ddprof-lib/src/main/cpp/faultInjection.cpp +++ b/ddprof-lib/src/main/cpp/faultInjection.cpp @@ -89,7 +89,7 @@ bool shouldFire(u64 threshold, const char* fn) { // place that counts an actually-injected fault. Counters::increment(FAULTS_INJECTED); #ifdef DEBUG - // Flag injections fired at a site with no walkVM longjmp protection active: + // Flag injections fired at a site with no walkVM siglongjmp protection active: // recovery there depends entirely on safefetch, and a raw deref would crash. ProfiledThread* t = ProfiledThread::current(); // never allocates if (t == nullptr || !t->isProtected()) { diff --git a/ddprof-lib/src/main/cpp/faultInjection.h b/ddprof-lib/src/main/cpp/faultInjection.h index f3e5ad1b64..e19f4bb1e9 100644 --- a/ddprof-lib/src/main/cpp/faultInjection.h +++ b/ddprof-lib/src/main/cpp/faultInjection.h @@ -20,7 +20,7 @@ // site (VMStructs::at, walkVM, walkFP, walkDwarf). When __FAULT_INJECTION__ is // defined, each wrapped expression, with the tier's probability, is replaced by // a deliberately bad address (so the load faults and the profiler's recovery -// path — SafeAccess safefetch or walkVM's setjmp/longjmp — is exercised) or a +// path — SafeAccess safefetch or walkVM's sigsetjmp/siglongjmp — is exercised) or a // random int/long value. When the flag is NOT defined, every macro is a strict // identity: it expands to exactly the parenthesized original expression, with // unchanged type and value category and zero runtime cost. diff --git a/ddprof-lib/src/main/cpp/guards.h b/ddprof-lib/src/main/cpp/guards.h index 4addd8d398..18bc4fbeda 100644 --- a/ddprof-lib/src/main/cpp/guards.h +++ b/ddprof-lib/src/main/cpp/guards.h @@ -90,14 +90,14 @@ class SignalHandlerScope { #define SIGNAL_HANDLER_GUARD() SignalHandlerScope _signal_handler_scope // Manually release the most recent SIGNAL_HANDLER_GUARD() before chaining to -// another handler that may longjmp through us (e.g. J9's SIGSEGV null-pointer +// another handler that may siglongjmp through us (e.g. J9's SIGSEGV null-pointer // check handler). After release(), depth has already been decremented; the // destructor becomes a no-op. #define SIGNAL_HANDLER_GUARD_RELEASE() _signal_handler_scope.release() -// Compensate for a longjmp that bypassed a SignalHandlerScope's destructor. -// Call at the setjmp landing point AFTER a known longjmp originated from -// within a signal handler frame (e.g. HotSpot's checkFault → longjmp recovery +// Compensate for a siglongjmp that bypassed a SignalHandlerScope's destructor. +// Call at the sigsetjmp landing point AFTER a known siglongjmp originated from +// within a signal handler frame (e.g. HotSpot's checkFault → siglongjmp recovery // in walkVM). void signalHandlerUnwindAfterLongjmp(); #define SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP() signalHandlerUnwindAfterLongjmp() diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index 6404d22be8..deb530a4aa 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -249,18 +249,18 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex sigjmp_buf crash_protection_ctx; - // Chaining jmp_buf + // Chaining sigjmp_buf // A non-signal-based-sampler can be interrupted by signal based sampler, // then we end up with multiple HotspotSupport::walkVM() calls on stack, - // each one sets up jmp_buf, they need to be chained to jump back to + // each one sets up sigjmp_buf, they need to be chained to jump back to // correct location. sigjmp_buf* prev_jmp_buf = prof_thread->getJmpCtx(); - // Should be preserved across setjmp/longjmp + // Should be preserved across sigsetjmp/siglongjmp volatile int depth = 0; int actual_max_depth = truncated ? max_depth + 1 : max_depth; if (sigsetjmp(crash_protection_ctx, 1) != 0) { - // checkFault() does a longjmp from inside segvHandler, bypassing + // checkFault() does a siglongjmp from inside segvHandler, bypassing // segvHandler's SignalHandlerScope destructor. Compensate. SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); prof_thread->setJmpCtx(prev_jmp_buf); @@ -378,7 +378,7 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex } // entry_fp has been range-checked by isValidFP above; any remaining // SIGSEGV from a stale/concurrently-freed pointer is caught by the - // setjmp crash protection in walkVM (checkFault -> longjmp). + // sigsetjmp crash protection in walkVM (checkFault -> siglongjmp). uintptr_t* carrier_fp_addr = (uintptr_t*)INJECT_FAULT_ADDRESS_UNLIKELY(entry_fp); uintptr_t carrier_fp = *carrier_fp_addr; const void* carrier_pc = ((const void**)carrier_fp_addr)[FRAME_PC_SLOT]; @@ -414,8 +414,8 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex // while PC is still in JVM stubs (JavaCalls, method entry/exit), we see CodeHeap // code without VMThread context. // - // Without vm_thread, crash protection via setjmp/longjmp cannot work - // (checkFault() needs vm_thread->exception() to longjmp). Any memory dereference in interpreter + // Without vm_thread, crash protection via sigsetjmp/siglongjmp cannot work + // (checkFault() needs vm_thread->exception() to siglongjmp). Any memory dereference in interpreter // frame handling or NMethod validation would crash the process with unrecoverable SEGV. // // The missing VMThread is a timing issue during thread lifecycle. diff --git a/ddprof-lib/src/main/cpp/hotspot/vmStructs.h b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h index 139d430a34..e09d035bbb 100644 --- a/ddprof-lib/src/main/cpp/hotspot/vmStructs.h +++ b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h @@ -33,11 +33,11 @@ class VMNMethod; // During stack walking in the profiler's signal handler, GC or class unloading // on another thread can free VMNMethod/VMMethod memory concurrently, making // pointers stale between the readability check and the actual dereference. -// In release builds the setjmp/longjmp crash protection in walkVM catches the +// In release builds the sigsetjmp/siglongjmp crash protection in walkVM catches the // resulting SIGSEGV. In debug builds the assert(isReadable) fires first, // sending SIGABRT which is uncatchable by crash protection. // When crash protection is active the assert is redundant — any bad read will -// be caught by the SIGSEGV handler and recovered via longjmp — so we skip it. +// be caught by the SIGSEGV handler and recovered via siglongjmp — so we skip it. // // Defined at the bottom of this file after VMThread is declared so that the // VMThread fallback path (isExceptionActive) is accessible without forward- diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 9494c8a15a..cb6ff9964e 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -971,15 +971,15 @@ void Profiler::disableEngines() { void Profiler::segvHandler(int signo, siginfo_t *siginfo, void *ucontext) { // J9 installs a SIGSEGV handler that uses siglongjmp() to recover from // null-pointer-check faults during normal Java execution. When we chain to - // it, that longjmp unwinds past our stack frame and skips the RAII + // it, that siglongjmp unwinds past our stack frame and skips the RAII // destructor, permanently leaking depth on the thread. Release the guard // before chaining so depth is correct whether the chained handler returns - // or longjmps. + // or siglongjmps. // // Sanitizer-coverage note: this also means depth == 0 inside the chained // handler, so DEBUG_ASSERT_NOT_IN_SIGNAL() will NOT fire for AS-unsafe // code reachable from a chained handler that returns normally. This is - // the lesser of two evils — leaking depth on longjmp would silently + // the lesser of two evils — leaking depth on siglongjmp would silently // break the production deferred-refresh gate, while the sanitizer gap // is bounded to third-party signal handler code we don't own. SIGNAL_HANDLER_GUARD(); @@ -987,7 +987,7 @@ void Profiler::segvHandler(int signo, siginfo_t *siginfo, void *ucontext) { return; // Handled — destructor decrements depth } SIGNAL_HANDLER_GUARD_RELEASE(); - // Not handled, chain to next handler (may longjmp; never return through us) + // Not handled, chain to next handler (may siglongjmp; never return through us) SigAction chain = OS::getSegvChainTarget(); if (chain != nullptr) { chain(signo, siginfo, ucontext); @@ -998,7 +998,7 @@ void Profiler::segvHandler(int signo, siginfo_t *siginfo, void *ucontext) { void Profiler::busHandler(int signo, siginfo_t *siginfo, void *ucontext) { // See segvHandler: release before chaining in case the chained handler - // longjmps through us. + // siglongjmps through us. SIGNAL_HANDLER_GUARD(); if (crashHandlerInternal(signo, siginfo, ucontext)) { return; // Handled — destructor decrements depth @@ -1026,7 +1026,7 @@ int Profiler::crashHandlerInternal(int signo, siginfo_t *siginfo, void *ucontext // Reentrancy protection: use TLS-based tracking if available. // If TLS is not available, the thread is not protected by - // longjmp, so bail out. + // siglongjmp, so bail out. bool have_tls_protection = false; if (thrd != nullptr) { if (!thrd->enterCrashHandler()) { diff --git a/ddprof-lib/src/main/cpp/safeAccess.h b/ddprof-lib/src/main/cpp/safeAccess.h index 7d78f2cc61..564743b153 100644 --- a/ddprof-lib/src/main/cpp/safeAccess.h +++ b/ddprof-lib/src/main/cpp/safeAccess.h @@ -119,9 +119,9 @@ class SafeAccess { #ifdef DEBUG private: // Debug diagnostic: bump a counter when a SafeAccess read/copy is issued while - // the current thread is already inside a walkVM longjmp-protected region, where + // the current thread is already inside a walkVM siglongjmp-protected region, where // the safefetch/safecopy overhead is redundant (a fault there is caught by the - // longjmp anyway). Defined out-of-line in safeAccess.cpp so this widely-included + // siglongjmp anyway). Defined out-of-line in safeAccess.cpp so this widely-included // header need not pull in threadLocalData.h / counters.h. isCopy selects the // SAFECOPY_WHILE_PROTECTED vs SAFEFETCH_WHILE_PROTECTED counter. static void countIfLongjmpProtected(bool isCopy); diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index 4a8da4a50a..de070521d3 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -58,7 +58,7 @@ class ProfiledThread : public ThreadLocalData { static void freeValue(void* value); static ThreadLocal _current_thread; - // longjmp buffer. Used by hotspot only at this moment. + // siglongjmp buffer. Used by hotspot only at this moment. // Published in walkVM() and consumed in checkFault() from an asynchronous // SEGV-handler context on the same thread; atomic makes the publish/observe // ordering explicit instead of relying on plain load/store, matching how diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index 9fccd78d71..6b84fd3434 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -61,7 +61,7 @@ static void fi_signal_wrapper(int signo, siginfo_t* siginfo, void* context) { if (SafeAccess::handle_safefetch(signo, context)) { return; // safefetch load recovered; PC already rewritten to _cont. } - Profiler::checkFault(ProfiledThread::current(), siginfo, context); // longjmp if protected + Profiler::checkFault(ProfiledThread::current(), siginfo, context); // setlongjmp if protected // Not protected and not a safefetch fault — real crash. if (signo == SIGBUS && orig_busHandler != nullptr) { orig_busHandler(signo, siginfo, context); @@ -145,7 +145,7 @@ TEST_F(FaultInjectionTest, SafeAccessRecoversFromInjectedFault) { } // (c2) walkVM path: a raw dereference of an injected poison pointer must be -// caught by the setjmp/longjmp crash protection, returning control to setjmp. +// caught by the sigsetjmp/siglongjmp crash protection, returning control to setjmp. TEST_F(FaultInjectionTest, WalkVmSetjmpRecoversFromInjectedFault) { ProfiledThread* t = ProfiledThread::current(); ASSERT_NE(t, nullptr); diff --git a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp index 5a8c03d67e..77d5555b69 100644 --- a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp +++ b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp @@ -10,7 +10,7 @@ * dereference JavaThread-only fields (anchor, vframe_top, …) on such threads. * VMThread::isJavaThread() provides the gate. * - * Crash recovery inside walkVM relies on setjmp/longjmp: + * Crash recovery inside walkVM relies on sigsetjmp/siglongjmp: * 1. walkVM stores a jmp_buf* on ProfiledThread (setJmpCtx/getJmpCtx), * chaining it with whatever context was already installed so a * signal-based sampler interrupting a non-signal-based sampler's own @@ -280,7 +280,7 @@ TEST_F(JmpCtxChainingTest, NestedFramesChainAndUnwindInOrder) { EXPECT_EQ(nullptr, _pt->getJmpCtx()); } -// End-to-end with real setjmp/longjmp: a fault inside the inner frame must +// End-to-end with real sigsetjmp/siglongjmp: a fault inside the inner frame must // land in the inner frame's own recovery branch — checkFault() always // longjmps through whatever is currently installed — and once the inner // frame has recovered and restored the outer's context, the outer frame must @@ -291,7 +291,7 @@ TEST_F(JmpCtxChainingTest, FaultInInnerFrameDoesNotDisturbOuterFrame) { int outer_landed = 0; int inner_landed = 0; - if (setjmp(outer_ctx) != 0) { + if (sigsetjmp(outer_ctx, 1) != 0) { outer_landed++; } else { _pt->setJmpCtx(&outer_ctx); @@ -301,14 +301,14 @@ TEST_F(JmpCtxChainingTest, FaultInInnerFrameDoesNotDisturbOuterFrame) { jmp_buf* inner_prev = _pt->getJmpCtx(); ASSERT_EQ(&outer_ctx, inner_prev); - if (setjmp(inner_ctx) != 0) { + if (sigsetjmp(inner_ctx, 1) != 0) { inner_landed++; _pt->setJmpCtx(inner_prev); } else { _pt->setJmpCtx(&inner_ctx); // Simulate checkFault(): longjmp through whatever is currently // installed — this must hit the inner frame, not the outer. - longjmp(*_pt->getJmpCtx(), 1); + siglongjmp(*_pt->getJmpCtx(), 1); FAIL() << "unreachable: longjmp does not return"; } // --- inner call has returned normally after recovering --- @@ -328,7 +328,7 @@ TEST_F(JmpCtxChainingTest, FaultInInnerFrameDoesNotDisturbOuterFrame) { // D. Profiler::checkFault() guard clauses // // This gtest binary has no live JVM attached, so JVMThread is not initialized -// and the longjmp path can't be exercised end-to-end here. +// and the siglongjmp path can't be exercised end-to-end here. // These tests still call the real checkFault() (not a replica) to lock down // its early-return guard: a null ProfiledThread* // --------------------------------------------------------------------------- From bdf0431b85ad94fee8f44469b208e25628611f22 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 24 Jul 2026 22:08:03 +0000 Subject: [PATCH 06/51] Fix --- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index 6b84fd3434..c0cd542df7 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -156,9 +156,9 @@ TEST_F(FaultInjectionTest, WalkVmSetjmpRecoversFromInjectedFault) { volatile size_t reads = 0; volatile size_t faults = 0; - jmp_buf ctx; - if (setjmp(ctx) != 0) { - recovered = true; // returned here via checkFault -> longjmp + sigjmp_buf ctx; + if (sigsetjmp(ctx) != 0) { + recovered = true; // returned here via checkFault -> siglongjmp faults++; } t->setJmpCtx(&ctx); From 008f443448fdac2bd9e46b669215e81a31950fe1 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 24 Jul 2026 22:16:41 +0000 Subject: [PATCH 07/51] Fix --- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index c0cd542df7..7ff99dbe0f 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -157,7 +157,7 @@ TEST_F(FaultInjectionTest, WalkVmSetjmpRecoversFromInjectedFault) { volatile size_t faults = 0; sigjmp_buf ctx; - if (sigsetjmp(ctx) != 0) { + if (sigsetjmp(ctx, 1) != 0) { recovered = true; // returned here via checkFault -> siglongjmp faults++; } From 5cf9771b5cbf8784cfb7f7392ce77e6945c983de Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sat, 25 Jul 2026 00:15:06 +0200 Subject: [PATCH 08/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/main/cpp/threadLocalData.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index de070521d3..293a8c15a8 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -239,7 +239,7 @@ class ProfiledThread : public ThreadLocalData { } inline void setJmpCtx(sigjmp_buf* buf) { - _jmp_buf = buf; + _jmp_buf = buf; } inline sigjmp_buf* getJmpCtx() const { From b4ddaf67c5689bdc23fd8915391f07db3cb9f99c Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sat, 25 Jul 2026 00:56:29 +0000 Subject: [PATCH 09/51] Fix test --- .../src/test/cpp/hotspot_crash_protection_ut.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp index 77d5555b69..241aaf973f 100644 --- a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp +++ b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp @@ -286,8 +286,8 @@ TEST_F(JmpCtxChainingTest, NestedFramesChainAndUnwindInOrder) { // frame has recovered and restored the outer's context, the outer frame must // be left exactly as it was, never having been unwound itself. TEST_F(JmpCtxChainingTest, FaultInInnerFrameDoesNotDisturbOuterFrame) { - jmp_buf outer_ctx; - jmp_buf* outer_prev = _pt->getJmpCtx(); + sigjmp_buf outer_ctx; + sigjmp_buf* outer_prev = _pt->getJmpCtx(); int outer_landed = 0; int inner_landed = 0; @@ -297,8 +297,8 @@ TEST_F(JmpCtxChainingTest, FaultInInnerFrameDoesNotDisturbOuterFrame) { _pt->setJmpCtx(&outer_ctx); // --- inner "walkVM" call, interrupted mid-flight by a fault --- - jmp_buf inner_ctx; - jmp_buf* inner_prev = _pt->getJmpCtx(); + sigjmp_buf inner_ctx; + sigjmp_buf* inner_prev = _pt->getJmpCtx(); ASSERT_EQ(&outer_ctx, inner_prev); if (sigsetjmp(inner_ctx, 1) != 0) { @@ -309,7 +309,7 @@ TEST_F(JmpCtxChainingTest, FaultInInnerFrameDoesNotDisturbOuterFrame) { // Simulate checkFault(): longjmp through whatever is currently // installed — this must hit the inner frame, not the outer. siglongjmp(*_pt->getJmpCtx(), 1); - FAIL() << "unreachable: longjmp does not return"; + FAIL() << "unreachable: siglongjmp does not return"; } // --- inner call has returned normally after recovering --- From df16d803855bee468ae131db528bfb9619d73228 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 24 Jul 2026 18:12:06 +0000 Subject: [PATCH 10/51] v0 --- ddprof-lib/src/main/cpp/profiler.cpp | 16 +++------------- ddprof-lib/src/main/cpp/profiler.h | 2 ++ ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 2 +- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index cb6ff9964e..d818f386cf 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -2038,24 +2038,14 @@ int Profiler::status(char* status, int max_len) { _alloc_engine != nullptr ? _alloc_engine->name() : "None"); } -void Profiler::checkFault(ProfiledThread* thrd, siginfo_t *siginfo, void *ucontext) { - // Check if longjmp is setup for this thread - if (thrd == nullptr || !thrd->isProtected()) { +(??) return; } - // Check if the fault is originated from java profiler - const uintptr_t pc = StackFrame(ucontext).pc(); - const uintptr_t min = (uintptr_t)profiler_min_address; - const uintptr_t max = (uintptr_t)profiler_max_address; - - // If the profiler address range is not initialized (e.g. unit tests), fall back - // to recovering unconditionally when a protection context is installed. - if ((min != 0 && max != 0) && (pc < min || pc >= max)) { - return; +(??) } thrd->resetCrashHandler(); Counters::increment(STACKWALK_LONGJMP_RECOVERED); - siglongjmp(*thrd->getJmpCtx(), 1); +(??) } diff --git a/ddprof-lib/src/main/cpp/profiler.h b/ddprof-lib/src/main/cpp/profiler.h index c4d2bf067a..0e4326bb8e 100644 --- a/ddprof-lib/src/main/cpp/profiler.h +++ b/ddprof-lib/src/main/cpp/profiler.h @@ -236,6 +236,8 @@ class alignas(alignof(SpinLock)) Profiler { return _instance; } + static void checkFault(ProfiledThread* thrd = nullptr); + // Resolve names of native (non-Java) threads from /proc. Idempotent and // allocation-light (no-op for already-named tids), so it is safe to call // periodically from the Libraries refresher thread to capture transient diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index 7ff99dbe0f..ea216cf37a 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -61,7 +61,7 @@ static void fi_signal_wrapper(int signo, siginfo_t* siginfo, void* context) { if (SafeAccess::handle_safefetch(signo, context)) { return; // safefetch load recovered; PC already rewritten to _cont. } - Profiler::checkFault(ProfiledThread::current(), siginfo, context); // setlongjmp if protected + Profiler::checkFault(ProfiledThread::current(), siginfo, context); // siglongjmp if protected // Not protected and not a safefetch fault — real crash. if (signo == SIGBUS && orig_busHandler != nullptr) { orig_busHandler(signo, siginfo, context); From 2a481a59c911c9ceb899975b8c908e438a999b87 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 24 Jul 2026 19:53:25 +0000 Subject: [PATCH 11/51] v1 --- ddprof-lib/src/main/cpp/profiler.cpp | 6 ++++-- ddprof-lib/src/main/cpp/profiler.h | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index d818f386cf..631d4466ac 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -2038,7 +2038,9 @@ int Profiler::status(char* status, int max_len) { _alloc_engine != nullptr ? _alloc_engine->name() : "None"); } -(??) +(??)void Profiler::checkFault(ProfiledThread* thrd) { +(??) // Should not get to here (?) +(??) if (thrd == nullptr) { return; } @@ -2047,5 +2049,5 @@ int Profiler::status(char* status, int max_len) { thrd->resetCrashHandler(); Counters::increment(STACKWALK_LONGJMP_RECOVERED); -(??) +(??) longjmp(*thrd->getJmpCtx(), 1); } diff --git a/ddprof-lib/src/main/cpp/profiler.h b/ddprof-lib/src/main/cpp/profiler.h index 0e4326bb8e..c4d2bf067a 100644 --- a/ddprof-lib/src/main/cpp/profiler.h +++ b/ddprof-lib/src/main/cpp/profiler.h @@ -236,8 +236,6 @@ class alignas(alignof(SpinLock)) Profiler { return _instance; } - static void checkFault(ProfiledThread* thrd = nullptr); - // Resolve names of native (non-Java) threads from /proc. Idempotent and // allocation-light (no-op for already-named tids), so it is safe to call // periodically from the Libraries refresher thread to capture transient From 7b5c6574f886a3f080601fef81e3ecaccc4276c8 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sat, 25 Jul 2026 20:13:55 -0400 Subject: [PATCH 12/51] Rebase --- ddprof-lib/src/main/cpp/profiler.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 631d4466ac..cb6ff9964e 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -2038,16 +2038,24 @@ int Profiler::status(char* status, int max_len) { _alloc_engine != nullptr ? _alloc_engine->name() : "None"); } -(??)void Profiler::checkFault(ProfiledThread* thrd) { -(??) // Should not get to here (?) -(??) if (thrd == nullptr) { +void Profiler::checkFault(ProfiledThread* thrd, siginfo_t *siginfo, void *ucontext) { + // Check if longjmp is setup for this thread + if (thrd == nullptr || !thrd->isProtected()) { return; } -(??) + // Check if the fault is originated from java profiler + const uintptr_t pc = StackFrame(ucontext).pc(); + const uintptr_t min = (uintptr_t)profiler_min_address; + const uintptr_t max = (uintptr_t)profiler_max_address; + + // If the profiler address range is not initialized (e.g. unit tests), fall back + // to recovering unconditionally when a protection context is installed. + if ((min != 0 && max != 0) && (pc < min || pc >= max)) { + return; } thrd->resetCrashHandler(); Counters::increment(STACKWALK_LONGJMP_RECOVERED); -(??) longjmp(*thrd->getJmpCtx(), 1); + siglongjmp(*thrd->getJmpCtx(), 1); } From 2be8182225905dfce8fa9776eec9796c772e64b8 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sun, 26 Jul 2026 00:36:21 +0000 Subject: [PATCH 13/51] Fix --- ddprof-lib/src/main/cpp/threadLocalData.h | 2 +- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 30 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index 293a8c15a8..106f0fcb9e 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -59,7 +59,7 @@ class ProfiledThread : public ThreadLocalData { static ThreadLocal _current_thread; // siglongjmp buffer. Used by hotspot only at this moment. - // Published in walkVM() and consumed in checkFault() from an asynchronous + // Published in walkVM()/recordSample() and consumed in checkFault() from an asynchronous // SEGV-handler context on the same thread; atomic makes the publish/observe // ordering explicit instead of relying on plain load/store, matching how // _crash_depth is hardened below. diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index ea216cf37a..32cda8546e 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -52,7 +52,7 @@ TEST(FaultInjectionTest, DisabledValueMacrosAreIdentity) { #else // __FAULT_INJECTION__ enabled (built under -PenableFaultInjection) -// Chain: safefetch recovery first, then walkVM setjmp/longjmp recovery, then +// Chain: safefetch recovery first, then walkVM sigsetjmp/siglongjmp recovery, then // the crash handler as a last resort so a genuine bug still produces a report. static void (*orig_segvHandler)(int, siginfo_t*, void*); static void (*orig_busHandler)(int, siginfo_t*, void*); @@ -145,7 +145,7 @@ TEST_F(FaultInjectionTest, SafeAccessRecoversFromInjectedFault) { } // (c2) walkVM path: a raw dereference of an injected poison pointer must be -// caught by the sigsetjmp/siglongjmp crash protection, returning control to setjmp. +// caught by the sigsetjmp/siglongjmp crash protection, returning control to sigsetjmp. TEST_F(FaultInjectionTest, WalkVmSetjmpRecoversFromInjectedFault) { ProfiledThread* t = ProfiledThread::current(); ASSERT_NE(t, nullptr); @@ -177,13 +177,13 @@ TEST_F(FaultInjectionTest, WalkVmSetjmpRecoversFromInjectedFault) { // We should have observed a recovered fault, or the loop completed cleanly. The // essential assertion is that the process did not die and, when a fault was - // injected, setjmp regained control. - EXPECT_GT(faults, 0u) << "expected at least one injected fault to longjmp-recover"; + // injected, sigsetjmp regained control. + EXPECT_GT(faults, 0u) << "expected at least one injected fault to siglongjmp-recover"; EXPECT_TRUE(recovered); SUCCEED(); } -// (c3) recordSample's outer setjmp region (PROF-15447): Profiler::recordSample() +// (c3) recordSample's outer sigsetjmp region (PROF-15447): Profiler::recordSample() // wraps the native/Java unwind in its own jmp_buf, chaining off whatever // context a caller may already have installed, so a fault in the // *unprotected* metadata reads surrounding walkVM/walkFP/walkDwarf (isJitCode, @@ -194,9 +194,9 @@ TEST_F(FaultInjectionTest, WalkVmSetjmpRecoversFromInjectedFault) { // recordSample() itself needs a live JVM (ASGCT, VMStructs, an allocated // _calltrace_buffer) unavailable in this gtest binary, so — following the // same "replicate the protocol" approach used elsewhere in this suite for -// JVM-dependent code — this test reproduces its exact save/install/setjmp/ -// longjmp/restore sequence verbatim, including the `volatile int num_frames` -// accumulator that must survive the longjmp landing, and drives it with the +// JVM-dependent code — this test reproduces its exact save/install/sigsetjmp/ +// siglongjmp/restore sequence verbatim, including the `volatile int num_frames` +// accumulator that must survive the siglongjmp landing, and drives it with the // same probabilistic fault injection as WalkVmSetjmpRecoversFromInjectedFault. // Unlike that test, this one also (a) starts from an already-installed // "grandparent" jmp_buf to verify nesting/chain-restore, matching how @@ -217,12 +217,12 @@ TEST_F(FaultInjectionTest, RecordSampleOuterSetjmpRecoversAndRestoresChain) { uintptr_t base = (uintptr_t)&real_slot; long long recovered_before = Counters::getCounter(STACKWALK_LONGJMP_RECOVERED); - // Read again after the setjmp landing below, so it must be volatile — mirrors + // Read again after the sigsetjmp landing below, so it must be volatile — mirrors // recordSample's own num_frames. volatile int num_frames = 0; // Frames "collected" before the fault, e.g. by a getNativeTrace() call that // partially succeeded before walkJavaStack() faulted. Seeding a non-zero - // value here — mutated between setjmp() and the longjmp below — is what + // value here — mutated between sigsetjmp() and the siglongjmp below — is what // actually exercises the volatile qualifier: a non-volatile local would be // indeterminate after the jump, so the post-recovery checks below would not // reliably see it. Without this seed, num_frames would still read 0 whether @@ -235,9 +235,9 @@ TEST_F(FaultInjectionTest, RecordSampleOuterSetjmpRecoversAndRestoresChain) { t->setFiRng(0xC0FFEEC0FFEEC0FFULL); - int jmp_rc = setjmp(unwind_ctx); + int jmp_rc = sigsetjmp(unwind_ctx, 1); if (jmp_rc != 0) { - // Landed here via the real Profiler::checkFault() -> longjmp, triggered by + // Landed here via the real Profiler::checkFault() -> siglongjmp, triggered by // an actual injected fault below. Mirrors recordSample's // `if (num_frames < _max_stack_depth) { num_frames += makeFrame(...); }`: // the recovery marker is appended to whatever partial progress survived @@ -246,12 +246,12 @@ TEST_F(FaultInjectionTest, RecordSampleOuterSetjmpRecoversAndRestoresChain) { num_frames += 1; // stand-in for makeFrame(..., "break_unwind_fault") } else { t->setJmpCtx(&unwind_ctx); - num_frames = kSeededFrames; // mutated before the fault; must survive the longjmp + num_frames = kSeededFrames; // mutated before the fault; must survive the siglongjmp // Force at least one fire deterministically, then let the tier drive the rest. for (int i = 0; i < 5000 && num_frames == kSeededFrames; i++) { // Raw deref of the (possibly poisoned) base — mirrors the unprotected - // metadata reads recordSample's outer setjmp now guards. + // metadata reads recordSample's outer sigsetjmp now guards. uintptr_t v = *(uintptr_t*)INJECT_FAULT_ADDRESS_LIKELY(base); asm volatile("" : "+r"(v) : : "memory"); } @@ -262,7 +262,7 @@ TEST_F(FaultInjectionTest, RecordSampleOuterSetjmpRecoversAndRestoresChain) { << "must restore the caller's jmp_buf chain on both the clean and the " "recovery path, never leave it cleared or pointing at unwind_ctx"; EXPECT_EQ(kSeededFrames + 1, num_frames) - << "the seeded pre-fault value must survive the longjmp landing and the " + << "the seeded pre-fault value must survive the siglongjmp landing and the " "recovery marker must append to it, not overwrite it"; EXPECT_GT(Counters::getCounter(STACKWALK_LONGJMP_RECOVERED), recovered_before) << "checkFault() must have run and incremented the shared recovery counter"; From 0df0f055f2da332f145ec98cccf2caabf7b1ab21 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sun, 26 Jul 2026 02:37:41 +0200 Subject: [PATCH 14/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index 32cda8546e..22063a823a 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -210,9 +210,8 @@ TEST_F(FaultInjectionTest, RecordSampleOuterSetjmpRecoversAndRestoresChain) { // Simulate a pre-existing outer protection context, as recordSample must // support when nested inside another sampler's own protected region. - jmp_buf grandparent_ctx; + sigjmp_buf grandparent_ctx; t->setJmpCtx(&grandparent_ctx); - uintptr_t real_slot = 0; // a valid, readable "metadata" slot uintptr_t base = (uintptr_t)&real_slot; long long recovered_before = Counters::getCounter(STACKWALK_LONGJMP_RECOVERED); From 9b6a9e18f5b73c7f601211fe81e43250b097b455 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sun, 26 Jul 2026 02:38:30 +0200 Subject: [PATCH 15/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/main/cpp/profiler.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index cb6ff9964e..006618dc0e 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -681,11 +681,7 @@ bool Profiler::recordSample(void *ucontext, u64 counter, int tid, Counters::increment(SAMPLES_DROPPED_THREAD_LOCAL); truncated = true; } else { - sigjmp_buf unwind_ctx; - sigjmp_buf *prev_jmp_buf = walk_thread->getJmpCtx(); - - int jmp_rc = jmp_rc = sigsetjmp(unwind_ctx, 1); - if (jmp_rc != 0) { + int jmp_rc = sigsetjmp(unwind_ctx, 1); // A fault during unwinding longjmp'd back here (via checkFault). The // longjmp bypassed segvHandler's SignalHandlerScope destructor, so // compensate, restore the previous jmp_buf chain, and record the From 82b31e9a2e3874666b88cd69bdf66d75041f395d Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sun, 26 Jul 2026 02:39:06 +0200 Subject: [PATCH 16/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/main/cpp/profiler.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 006618dc0e..920692457f 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -1108,11 +1108,11 @@ void Profiler::setupSignalHandlers() { // Get address range of java profiler library Libraries* libs = Libraries::instance(); - CodeCache* prof_lib = libs->findLibraryByName("libjavaProfiler"); - assert(prof_lib != nullptr); - profiler_min_address = prof_lib->minAddress(); - profiler_max_address = prof_lib->maxAddress(); - assert(profiler_min_address != nullptr && profiler_max_address != nullptr); + CodeCache* prof_lib = libs->findLibraryByAddress((const void*)&Profiler::setupSignalHandlers); + if (prof_lib != nullptr) { + profiler_min_address = prof_lib->minAddress(); + profiler_max_address = prof_lib->maxAddress(); + } #ifdef __FAULT_INJECTION__ // Reserve the PROT_NONE guard region used to poison memory-access sites. From 46c434111c9e5cf7e52ccd78716580edcf35ac43 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sun, 26 Jul 2026 02:39:35 +0200 Subject: [PATCH 17/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/main/cpp/profiler.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 920692457f..41ef970674 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -2035,6 +2035,7 @@ int Profiler::status(char* status, int max_len) { } void Profiler::checkFault(ProfiledThread* thrd, siginfo_t *siginfo, void *ucontext) { + (void)siginfo; // Check if longjmp is setup for this thread if (thrd == nullptr || !thrd->isProtected()) { return; From 728559c6063857efd17d8b2efe79af2fccf63908 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sun, 26 Jul 2026 00:47:30 +0000 Subject: [PATCH 18/51] Fix --- ddprof-lib/src/main/cpp/profiler.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 41ef970674..16955bcdb7 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -681,10 +681,13 @@ bool Profiler::recordSample(void *ucontext, u64 counter, int tid, Counters::increment(SAMPLES_DROPPED_THREAD_LOCAL); truncated = true; } else { + sigjmp_buf unwind_ctx; + sigjmp_buf *prev_jmp_buf = walk_thread->getJmpCtx(); int jmp_rc = sigsetjmp(unwind_ctx, 1); + if (jmp_rc != 0) { // A fault during unwinding longjmp'd back here (via checkFault). The - // longjmp bypassed segvHandler's SignalHandlerScope destructor, so - // compensate, restore the previous jmp_buf chain, and record the + // siglongjmp bypassed segvHandler's SignalHandlerScope destructor, so + // compensate, restore the previous sigjmp_buf chain, and record the // partial trace with an error marker instead of crashing. SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); walk_thread->setJmpCtx(prev_jmp_buf); From da218ef2de7b7db45c5a12b4c75820bb92882bcd Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sun, 26 Jul 2026 00:57:04 +0000 Subject: [PATCH 19/51] Fix comments --- ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp | 10 +++++----- ddprof-lib/src/main/cpp/profiler.cpp | 14 +++++++------- ddprof-lib/src/main/cpp/signalInflight.h | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp b/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp index 1c08286d22..e458fe6a1c 100644 --- a/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp +++ b/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp @@ -100,13 +100,13 @@ static void cleanup_unregister(void*) { // // The fix: use __pthread_register_cancel / __pthread_unregister_cancel // directly — the same thing the C macro form of pthread_cleanup_push does. -// This registers cleanup via a setjmp buffer in a runtime linked-list, NOT +// This registers cleanup via a sigsetjmp buffer in a runtime linked-list, NOT // via an LSDA destructor. _Unwind_ForcedUnwind's stop function // (__pthread_unwind_stop) handles the cleanup without ever calling // __gxx_personality_v0 for this frame, so _Unwind_SetGR is never called and // the cross-version incompatibility is never triggered. // -// On musl: pthread_cleanup_push already uses the C/setjmp form (no RAII), +// On musl: pthread_cleanup_push already uses the C/sigsetjmp form (no RAII), // and pthread_exit does not use _Unwind_ForcedUnwind, so there is no issue. // The __GLIBC__ guard keeps the musl path unchanged. #ifdef __GLIBC__ @@ -131,7 +131,7 @@ void run_with_cleanup(func_start_routine routine, void* params, static_assert(offsetof(__pthread_unwind_buf_t, __cancel_jmp_buf) == 0 && sizeof(cancel_buf.__cancel_jmp_buf[0]) == offsetof(struct __jmp_buf_tag, __saved_mask), "glibc __pthread_unwind_buf_t inner layout incompatible with struct __jmp_buf_tag"); - // __sigsetjmp/longjmp only intercepts _Unwind_ForcedUnwind (pthread_exit / + // __sigsetjmp/siglongjmp only intercepts _Unwind_ForcedUnwind (pthread_exit / // cancellation). routine(params) must NOT throw a regular C++ exception // across this boundary: an escaping exception would skip both // __pthread_unregister_cancel and cleanup_fn below, leaking the thread @@ -144,7 +144,7 @@ void run_with_cleanup(func_start_routine routine, void* params, // set __sigsetjmp's savemask=0 (the second parameter, noting that the signal mask is NOT // saved/restored, which is correct because the cancel mechanism does not depend on signal mask state. __sigsetjmp((struct __jmp_buf_tag*)(void*)cancel_buf.__cancel_jmp_buf, 0), 0)) { - // Reached via longjmp from glibc's stop function when pthread_exit + // Reached via siglongjmp from glibc's stop function when pthread_exit // (or cancellation) fires. Run cleanup and continue unwinding. cleanup_fn(cleanup_arg); __pthread_unwind_next(&cancel_buf); @@ -163,7 +163,7 @@ void run_with_cleanup(func_start_routine routine, void* params, __pthread_unregister_cancel(&cancel_buf); cleanup_fn(cleanup_arg); #else - // musl / non-glibc: pthread_cleanup_push uses the C/setjmp form, no RAII. + // musl / non-glibc: pthread_cleanup_push uses the C/sigsetjmp form, no RAII. pthread_cleanup_push(cleanup_fn, cleanup_arg); routine(params); pthread_cleanup_pop(1); diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 16955bcdb7..623888b0a5 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -659,12 +659,12 @@ bool Profiler::recordSample(void *ucontext, u64 counter, int tid, #endif // COUNTERS ASGCT_CallFrame *frames = _calltrace_buffer[lock_index]->_asgct_frames; - // Read again after the setjmp landing below, so it must be volatile: a - // longjmp out of the unwind leaves non-volatile locals indeterminate. + // Read again after the sigsetjmp landing below, so it must be volatile: a + // siglongjmp out of the unwind leaves non-volatile locals indeterminate. volatile int num_frames = 0; StackContext java_ctx = {0}; - // Establish setjmp/longjmp crash protection around the unwind. The native + // Establish sigsetjmp/siglongjmp crash protection around the unwind. The native // walkers (walkFP/walkDwarf) protect their pointer loads with SafeAccess // safefetch, but the surrounding metadata reads (isJitCode, findFrameDesc, // findLibraryByAddress, AGCT in getJavaTraceAsync, frame.link, ...) are raw @@ -685,7 +685,7 @@ bool Profiler::recordSample(void *ucontext, u64 counter, int tid, sigjmp_buf *prev_jmp_buf = walk_thread->getJmpCtx(); int jmp_rc = sigsetjmp(unwind_ctx, 1); if (jmp_rc != 0) { - // A fault during unwinding longjmp'd back here (via checkFault). The + // A fault during unwinding siglongjmp'd back here (via checkFault). The // siglongjmp bypassed segvHandler's SignalHandlerScope destructor, so // compensate, restore the previous sigjmp_buf chain, and record the // partial trace with an error marker instead of crashing. @@ -698,7 +698,7 @@ bool Profiler::recordSample(void *ucontext, u64 counter, int tid, } else { walk_thread->setJmpCtx(&unwind_ctx); - // truncated_local is never read after a longjmp landing (only on the + // truncated_local is never read after a siglongjmp landing (only on the // clean path below), so it need not be volatile; the outer `truncated` // stays false on the recovery path. bool truncated_local = false; @@ -1055,7 +1055,7 @@ int Profiler::crashHandlerInternal(int signo, siginfo_t *siginfo, void *ucontext } // Profiler::checkFault has its own check if we're in a protected stack walk. - // If the fault is from our protected walk, it will longjmp and never return. + // If the fault is from our protected walk, it will siglongjmp and never return. // If it returns, the fault wasn't from our code. Profiler::checkFault(thrd, siginfo, ucontext); @@ -2039,7 +2039,7 @@ int Profiler::status(char* status, int max_len) { void Profiler::checkFault(ProfiledThread* thrd, siginfo_t *siginfo, void *ucontext) { (void)siginfo; - // Check if longjmp is setup for this thread + // Check if siglongjmp is setup for this thread if (thrd == nullptr || !thrd->isProtected()) { return; } diff --git a/ddprof-lib/src/main/cpp/signalInflight.h b/ddprof-lib/src/main/cpp/signalInflight.h index 2855af1c2e..6217290dee 100644 --- a/ddprof-lib/src/main/cpp/signalInflight.h +++ b/ddprof-lib/src/main/cpp/signalInflight.h @@ -29,12 +29,12 @@ // writes do not invalidate the cache line backing each engine's _enabled // flag, which is read on every signal. // -// Known limitation — longjmp out of a signal handler frame: -// If a signal handler frame is unwound by a longjmp that bypasses the +// Known limitation — siglongjmp out of a signal handler frame: +// If a signal handler frame is unwound by a siglongjmp that bypasses the // InflightGuard destructor, the counter leaks by +1 permanently. In this // codebase that can only happen via J9's SIGSEGV null-pointer-check // handler: our segvHandler chains to J9 for unclaimed faults, and J9 may -// siglongjmp to a setjmp installed in normal Java code (J9 null-check +// siglongjmp to a sigsetjmp installed in normal Java code (J9 null-check // recovery), unwinding past every frame above it including any active // InflightGuard. SignalHandlerScope has the same limitation for its own // depth counter (see guards.h) and the codebase accepts it. From 741eb8e51c4dd2da14d02467b783a7b6f5c661e4 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sun, 26 Jul 2026 01:39:30 +0000 Subject: [PATCH 20/51] Fix test --- .../test/cpp/hotspot_crash_protection_ut.cpp | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp index 241aaf973f..36f5207348 100644 --- a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp +++ b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp @@ -11,7 +11,7 @@ * VMThread::isJavaThread() provides the gate. * * Crash recovery inside walkVM relies on sigsetjmp/siglongjmp: - * 1. walkVM stores a jmp_buf* on ProfiledThread (setJmpCtx/getJmpCtx), + * 1. walkVM stores a sigjmp_buf* on ProfiledThread (setJmpCtx/getJmpCtx), * chaining it with whatever context was already installed so a * signal-based sampler interrupting a non-signal-based sampler's own * in-flight walkVM() call doesn't clobber the outer call's context. @@ -24,7 +24,7 @@ * Tests cover: * A. ProfiledThread thread-type classification (isJavaThread fast path) * B. Crash-handler nesting depth (ProfiledThread crash handler state) - * C. jmp_buf chaining across nested/interrupted walkVM() calls + * C. sigjmp_buf chaining across nested/interrupted walkVM() calls */ #include @@ -190,12 +190,12 @@ TEST_F(CrashHandlerNestingTest, IsDeepOnlyAboveLimit) { } // --------------------------------------------------------------------------- -// C. jmp_buf chaining (ProfiledThread::setJmpCtx/getJmpCtx/isProtected) +// C. sigjmp_buf chaining (ProfiledThread::setJmpCtx/getJmpCtx/isProtected) // // A non-signal-based sampler's walkVM() call can itself be interrupted by a // signal-based sampler, putting two walkVM() frames on the same thread's // stack. Each frame follows the same protocol: -// jmp_buf* prev = prof_thread->getJmpCtx(); // save whatever was there +// sigjmp_buf* prev = prof_thread->getJmpCtx(); // save whatever was there // prof_thread->setJmpCtx(&my_ctx); // install this frame's ctx // ... walk ... // prof_thread->setJmpCtx(prev); // restore on every exit path @@ -227,7 +227,7 @@ TEST_F(JmpCtxChainingTest, InitiallyUnprotected) { } TEST_F(JmpCtxChainingTest, SetAndGetRoundTrip) { - jmp_buf ctx; + sigjmp_buf ctx; _pt->setJmpCtx(&ctx); EXPECT_TRUE(_pt->isProtected()); EXPECT_EQ(&ctx, _pt->getJmpCtx()); @@ -235,8 +235,8 @@ TEST_F(JmpCtxChainingTest, SetAndGetRoundTrip) { // Replicates a single walkVM() call's save/install/restore around its body. TEST_F(JmpCtxChainingTest, SingleFrameRestoresPreviousOnExit) { - jmp_buf outer; - jmp_buf* prev = _pt->getJmpCtx(); // nullptr: no enclosing walkVM() call + sigjmp_buf outer; + sigjmp_buf* prev = _pt->getJmpCtx(); // nullptr: no enclosing walkVM() call ASSERT_EQ(nullptr, prev); _pt->setJmpCtx(&outer); @@ -253,8 +253,8 @@ TEST_F(JmpCtxChainingTest, SingleFrameRestoresPreviousOnExit) { // chain off the outer's jmp_buf*, install its own, and hand the outer's back // on its way out — leaving the outer frame's context exactly as it left it. TEST_F(JmpCtxChainingTest, NestedFramesChainAndUnwindInOrder) { - jmp_buf outer_ctx; - jmp_buf* outer_prev = _pt->getJmpCtx(); + sigjmp_buf outer_ctx; + sigjmp_buf* outer_prev = _pt->getJmpCtx(); ASSERT_EQ(nullptr, outer_prev); _pt->setJmpCtx(&outer_ctx); EXPECT_EQ(&outer_ctx, _pt->getJmpCtx()); @@ -262,8 +262,8 @@ TEST_F(JmpCtxChainingTest, NestedFramesChainAndUnwindInOrder) { { // Inner walkVM() call, as if a signal fired while the outer one was // mid-walk. - jmp_buf inner_ctx; - jmp_buf* inner_prev = _pt->getJmpCtx(); + sigjmp_buf inner_ctx; + sigjmp_buf* inner_prev = _pt->getJmpCtx(); EXPECT_EQ(&outer_ctx, inner_prev); // chained off the outer frame _pt->setJmpCtx(&inner_ctx); From 85d0655d68853a1bee779da27e9912778b6cffc9 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sun, 26 Jul 2026 03:39:39 +0200 Subject: [PATCH 21/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/main/cpp/profiler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 623888b0a5..2e76534a28 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -1079,8 +1079,8 @@ int Profiler::crashHandlerInternal(int signo, siginfo_t *siginfo, void *ucontext return 0; // not handled, safe to chain } -static const void* profiler_min_address = nullptr; -static const void* profiler_max_address = nullptr; +static std::atomic profiler_min_address{0}; +static std::atomic profiler_max_address{0}; void Profiler::setupSignalHandlers() { // Do not re-run the signal setup (run only when VM has not been loaded yet) From bed222c9881d0ae811abdc404fe55b9f93d2fc9d Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sun, 26 Jul 2026 03:41:07 +0200 Subject: [PATCH 22/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index deb530a4aa..cf4c2d6e7c 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -414,9 +414,9 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex // while PC is still in JVM stubs (JavaCalls, method entry/exit), we see CodeHeap // code without VMThread context. // - // Without vm_thread, crash protection via sigsetjmp/siglongjmp cannot work - // (checkFault() needs vm_thread->exception() to siglongjmp). Any memory dereference in interpreter - // frame handling or NMethod validation would crash the process with unrecoverable SEGV. + // Without vm_thread, we can't safely walk interpreter frames or validate nmethods for + // JVM-generated (CodeHeap) code. Any memory dereference there may crash the process + // with an unrecoverable SEGV. // // The missing VMThread is a timing issue during thread lifecycle. if (vm_thread == NULL) { From 468d05f601c4aa6cf9836b540b8e98abde5f204b Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 12:52:32 +0000 Subject: [PATCH 23/51] Fix --- ddprof-lib/src/main/cpp/profiler.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 623888b0a5..5db39e658c 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -1112,10 +1112,9 @@ void Profiler::setupSignalHandlers() { // Get address range of java profiler library Libraries* libs = Libraries::instance(); CodeCache* prof_lib = libs->findLibraryByAddress((const void*)&Profiler::setupSignalHandlers); - if (prof_lib != nullptr) { - profiler_min_address = prof_lib->minAddress(); - profiler_max_address = prof_lib->maxAddress(); - } + assert(prof_lib != nullptr); + profiler_min_address = prof_lib->minAddress(); + profiler_max_address = prof_lib->maxAddress(); #ifdef __FAULT_INJECTION__ // Reserve the PROT_NONE guard region used to poison memory-access sites. @@ -2038,17 +2037,21 @@ int Profiler::status(char* status, int max_len) { } void Profiler::checkFault(ProfiledThread* thrd, siginfo_t *siginfo, void *ucontext) { - (void)siginfo; + (void)ucontext; // Check if siglongjmp is setup for this thread if (thrd == nullptr || !thrd->isProtected()) { return; } // Check if the fault is originated from java profiler - const uintptr_t pc = StackFrame(ucontext).pc(); + const uintptr_t pc = (uintptr_t)siginfo->si_addr; const uintptr_t min = (uintptr_t)profiler_min_address; const uintptr_t max = (uintptr_t)profiler_max_address; + #if !defined(UNIT_TEST) + assert(min != 0 && max != 0); + #endif + // If the profiler address range is not initialized (e.g. unit tests), fall back // to recovering unconditionally when a protection context is installed. if ((min != 0 && max != 0) && (pc < min || pc >= max)) { From 1f6c6d50d9a04f1554efbfcc1eb47affc3c309ec Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Tue, 28 Jul 2026 19:05:16 +0000 Subject: [PATCH 24/51] fix --- ddprof-lib/src/main/cpp/profiler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 03f9c62f3c..6caa64f55b 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -1113,8 +1113,8 @@ void Profiler::setupSignalHandlers() { Libraries* libs = Libraries::instance(); CodeCache* prof_lib = libs->findLibraryByAddress((const void*)&Profiler::setupSignalHandlers); assert(prof_lib != nullptr); - profiler_min_address = prof_lib->minAddress(); - profiler_max_address = prof_lib->maxAddress(); + profiler_min_address = reinterpret_cast(prof_lib->minAddress()); + profiler_max_address = reinterpret_cast(prof_lib->maxAddress()); #ifdef __FAULT_INJECTION__ // Reserve the PROT_NONE guard region used to poison memory-access sites. From 2b47e15f64dcf0133391b5a6e15fdd3338f61d06 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Tue, 28 Jul 2026 20:27:58 +0000 Subject: [PATCH 25/51] Longjmp protection leaver --- .../src/main/cpp/hotspot/hotspotSupport.cpp | 21 ++++++++++++------- .../src/main/cpp/hotspot/hotspotSupport.h | 1 + ddprof-lib/src/main/cpp/jvmSupport.cpp | 16 ++++++++++++++ ddprof-lib/src/main/cpp/jvmSupport.h | 11 ++++++++++ ddprof-lib/src/main/cpp/profiler.cpp | 2 +- ddprof-lib/src/main/cpp/profiler.h | 8 ------- 6 files changed, 42 insertions(+), 17 deletions(-) diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index cf4c2d6e7c..9b0d22f96e 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -978,6 +978,11 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex return depth; } +void HotspotSupport::JVMAsyncGetCallTrace(ASGCT_CallTrace* traces, jint depth, void* ucontext) { + LongjmpProtectionLeaver leaver(ProfiledThread::current()); + VM::_asyncGetCallTrace(traces, depth, ucontext); +} + int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, int max_depth, StackContext *java_ctx, bool *truncated) { @@ -1055,7 +1060,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, JitWriteProtection jit(false); // AsyncGetCallTrace writes to ASGCT_CallFrame array ASGCT_CallTrace trace = {jni, 0, frames}; - VM::_asyncGetCallTrace(&trace, max_depth, ucontext); + JVMAsyncGetCallTrace(&trace, max_depth, ucontext); if (trace.num_frames > 0) { frame.restore(saved_pc, saved_sp, saved_fp); @@ -1076,7 +1081,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, if (!(safe_mode & POP_STUB) && frame.unwindStub((instruction_t *)stub->_start, stub->_name) && isAddressInCode((const void *)frame.pc())) { - VM::_asyncGetCallTrace(&trace, max_depth, ucontext); + JVMAsyncGetCallTrace(&trace, max_depth, ucontext); } } else if (VMStructs::hasMethodStructs()) { VMNMethod *nmethod = CodeHeap::findNMethod((const void *)frame.pc()); @@ -1089,7 +1094,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, } if (!(safe_mode & POP_METHOD) && frame.unwindCompiled(nmethod) && isAddressInCode((const void *)frame.pc())) { - VM::_asyncGetCallTrace(&trace, max_depth, ucontext); + JVMAsyncGetCallTrace(&trace, max_depth, ucontext); } if ((safe_mode & PROBE_SP) && trace.num_frames < 0) { if (isValidJMethodID(method_id)) { @@ -1097,7 +1102,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, } for (int i = 0; trace.num_frames < 0 && i < PROBE_SP_LIMIT; i++) { frame.sp() += sizeof(void*); - VM::_asyncGetCallTrace(&trace, max_depth, ucontext); + JVMAsyncGetCallTrace(&trace, max_depth, ucontext); } } } @@ -1109,7 +1114,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, if (!(safe_mode & POP_STUB) && frame.unwindStub(NULL, nmethod->name()) && isAddressInCode((const void *)frame.pc())) { - VM::_asyncGetCallTrace(&trace, max_depth, ucontext); + JVMAsyncGetCallTrace(&trace, max_depth, ucontext); } } } @@ -1136,9 +1141,9 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, m->frameCompleteOffset() == -1) { m->setFrameCompleteOffset(0); } - VM::_asyncGetCallTrace(&trace, max_depth, ucontext); + JVMAsyncGetCallTrace(&trace, max_depth, ucontext); } else if (libs->findLibraryByAddress(pc) != NULL) { - VM::_asyncGetCallTrace(&trace, max_depth, ucontext); + JVMAsyncGetCallTrace(&trace, max_depth, ucontext); } anchor->setLastJavaPC(nullptr); @@ -1156,7 +1161,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, if (m != NULL && !m->isNMethod() && m->frameSize() > 0 && m->frameCompleteOffset() == -1) { m->setFrameCompleteOffset(0); - VM::_asyncGetCallTrace(&trace, max_depth, ucontext); + JVMAsyncGetCallTrace(&trace, max_depth, ucontext); } } } else if (trace.num_frames == ticks_GC_active && !(safe_mode & GC_TRACES)) { diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h index 6aed4ff00d..65e0edf9da 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h @@ -31,6 +31,7 @@ class HotspotSupport { static int getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, int max_depth, StackContext *java_ctx, bool *truncated); + static void JVMAsyncGetCallTrace(ASGCT_CallTrace *, jint, void *); static bool loadMethodIDsIfNeededImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass, bool load_all); public: diff --git a/ddprof-lib/src/main/cpp/jvmSupport.cpp b/ddprof-lib/src/main/cpp/jvmSupport.cpp index 783c34e458..7612985982 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.cpp +++ b/ddprof-lib/src/main/cpp/jvmSupport.cpp @@ -106,6 +106,8 @@ int JVMSupport::asyncGetCallTrace(ASGCT_CallFrame *frames, int max_depth, void* return 0; } + LongjmpProtectionLeaver leaver(ProfiledThread::current()); + JitWriteProtection jit(false); // AsyncGetCallTrace writes to ASGCT_CallFrame array ASGCT_CallTrace trace = {jni, 0, frames}; @@ -176,3 +178,17 @@ bool JVMSupport::loadMethodIDsImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass) { } return false; } + +LongjmpProtectionLeaver::LongjmpProtectionLeaver(ProfiledThread* const thread) : + _thread(thread), _jmp_buf(nullptr) { + if (thread != nullptr) { + _jmp_buf = thread->getJmpCtx(); + thread->setJmpCtx(nullptr); + } +} + +LongjmpProtectionLeaver::~LongjmpProtectionLeaver() { + if (_thread != nullptr) { + _thread->setJmpCtx(_jmp_buf); + } +} diff --git a/ddprof-lib/src/main/cpp/jvmSupport.h b/ddprof-lib/src/main/cpp/jvmSupport.h index f3d664d2dc..e41a64dfdc 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.h +++ b/ddprof-lib/src/main/cpp/jvmSupport.h @@ -10,6 +10,8 @@ #include "stackFrame.h" #include "stackWalker.h" +#include + // Stack recovery techniques used to workaround AsyncGetCallTrace flaws. // Can be disabled with 'safemode' option. enum StackRecovery { @@ -69,4 +71,13 @@ class JVMSupport { static inline bool isHidden(jint modifiers); }; +class LongjmpProtectionLeaver { +private: + sigjmp_buf* _jmp_buf; + ProfiledThread* const _thread; +public: + LongjmpProtectionLeaver(ProfiledThread* const thread); + ~LongjmpProtectionLeaver(); +}; + #endif // _JVMSUPPORT_H diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 6caa64f55b..e9c1847218 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -2044,7 +2044,7 @@ void Profiler::checkFault(ProfiledThread* thrd, siginfo_t *siginfo, void *uconte } // Check if the fault is originated from java profiler - const uintptr_t pc = (uintptr_t)siginfo->si_addr; + const uintptr_t pc = (uintptr_t)StackFrame(ucontext).pc(); const uintptr_t min = (uintptr_t)profiler_min_address; const uintptr_t max = (uintptr_t)profiler_max_address; diff --git a/ddprof-lib/src/main/cpp/profiler.h b/ddprof-lib/src/main/cpp/profiler.h index c4d2bf067a..df20d53d1c 100644 --- a/ddprof-lib/src/main/cpp/profiler.h +++ b/ddprof-lib/src/main/cpp/profiler.h @@ -498,14 +498,6 @@ class alignas(alignof(SpinLock)) Profiler { // Keep backward compatibility with the upstream async-profiler inline CodeCache* findLibraryByAddress(const void *address) { - #ifdef DEBUG - // we need this code to simulate segfault during stackwalking - // this is a safe place to do it since this wrapper is used solely from the 'vm' stackwalker implementation - if (force_stackwalk_crash_env) { - TEST_LOG("FORCE_SIGSEGV"); - raise(SIGSEGV); - } - #endif return Libraries::instance()->findLibraryByAddress(address); } From c24098307e51ff9f28bf6dd19efa5efdaad2af8e Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 29 Jul 2026 02:24:08 +0200 Subject: [PATCH 26/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp index 36f5207348..e9378ac7fd 100644 --- a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp +++ b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp @@ -282,7 +282,7 @@ TEST_F(JmpCtxChainingTest, NestedFramesChainAndUnwindInOrder) { // End-to-end with real sigsetjmp/siglongjmp: a fault inside the inner frame must // land in the inner frame's own recovery branch — checkFault() always -// longjmps through whatever is currently installed — and once the inner +// siglongjmps through whatever is currently installed — and once the inner // frame has recovered and restored the outer's context, the outer frame must // be left exactly as it was, never having been unwound itself. TEST_F(JmpCtxChainingTest, FaultInInnerFrameDoesNotDisturbOuterFrame) { From dd2d48cc866123ce5c9ba435859789f40551001c Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 29 Jul 2026 02:24:24 +0200 Subject: [PATCH 27/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index 22063a823a..469236a455 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -184,13 +184,12 @@ TEST_F(FaultInjectionTest, WalkVmSetjmpRecoversFromInjectedFault) { } // (c3) recordSample's outer sigsetjmp region (PROF-15447): Profiler::recordSample() -// wraps the native/Java unwind in its own jmp_buf, chaining off whatever +// wraps the native/Java unwind in its own sigjmp_buf, chaining off whatever // context a caller may already have installed, so a fault in the // *unprotected* metadata reads surrounding walkVM/walkFP/walkDwarf (isJitCode, // findFrameDesc, findLibraryByAddress, AGCT in getJavaTraceAsync, frame.link, // ...) recovers to a partial trace instead of crashing — and, critically, -// restores the caller's jmp_buf chain on both the clean and the recovery path. -// +// restores the caller's sigjmp_buf chain on both the clean and the recovery path. // recordSample() itself needs a live JVM (ASGCT, VMStructs, an allocated // _calltrace_buffer) unavailable in this gtest binary, so — following the // same "replicate the protocol" approach used elsewhere in this suite for From 39cf23d4e298010073114b58ed0e82e6d0c9dda6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 05:23:16 +0000 Subject: [PATCH 28/51] build(deps): bump the codeql-action group with 2 updates (#688) Bumps the codeql-action group with 2 updates: [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action). Updates `github/codeql-action/init` from 4.37.1 to 4.37.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/7188fc363630916deb702c7fdcf4e481b751f97a...e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81) Updates `github/codeql-action/analyze` from 4.37.1 to 4.37.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/7188fc363630916deb702c7fdcf4e481b751f97a...e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.37.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: codeql-action - dependency-name: github/codeql-action/analyze dependency-version: 4.37.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: codeql-action ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codecheck.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codecheck.yml b/.github/workflows/codecheck.yml index 213a307090..44e532c1df 100644 --- a/.github/workflows/codecheck.yml +++ b/.github/workflows/codecheck.yml @@ -76,7 +76,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 + uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -85,4 +85,4 @@ jobs: # queries: ./path/to/local/query, your-org/your-repo/queries@main - run: ./gradlew -x test assembleReleaseJar - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 + uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 From 73bd5693477a0a15f2f42cf1b22ef7b2325e57c9 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Tue, 28 Jul 2026 20:32:18 -0400 Subject: [PATCH 29/51] Merge --- .../src/main/cpp/hotspot/hotspotSupport.cpp | 6 +- ddprof-lib/src/main/cpp/profiler.cpp | 77 ++++------------ ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 88 +------------------ .../test/cpp/hotspot_crash_protection_ut.cpp | 14 +-- 4 files changed, 30 insertions(+), 155 deletions(-) diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index 9b0d22f96e..6a926906cc 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -414,9 +414,9 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex // while PC is still in JVM stubs (JavaCalls, method entry/exit), we see CodeHeap // code without VMThread context. // - // Without vm_thread, we can't safely walk interpreter frames or validate nmethods for - // JVM-generated (CodeHeap) code. Any memory dereference there may crash the process - // with an unrecoverable SEGV. + // Without vm_thread, crash protection via sigsetjmp/siglongjmp cannot work + // (checkFault() needs vm_thread->exception() to siglongjmp). Any memory dereference in interpreter + // frame handling or NMethod validation would crash the process with unrecoverable SEGV. // // The missing VMThread is a timing issue during thread lifecycle. if (vm_thread == NULL) { diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index e9c1847218..8f6cf1f7dd 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -659,67 +659,23 @@ bool Profiler::recordSample(void *ucontext, u64 counter, int tid, #endif // COUNTERS ASGCT_CallFrame *frames = _calltrace_buffer[lock_index]->_asgct_frames; - // Read again after the sigsetjmp landing below, so it must be volatile: a - // siglongjmp out of the unwind leaves non-volatile locals indeterminate. - volatile int num_frames = 0; - StackContext java_ctx = {0}; - - // Establish sigsetjmp/siglongjmp crash protection around the unwind. The native - // walkers (walkFP/walkDwarf) protect their pointer loads with SafeAccess - // safefetch, but the surrounding metadata reads (isJitCode, findFrameDesc, - // findLibraryByAddress, AGCT in getJavaTraceAsync, frame.link, ...) are raw - // dereferences of signal-supplied pc/sp/fp. Without an active jmp_buf a - // fault there is unrecoverable: crashHandlerInternal -> checkFault() finds - // isProtected()==false and chains to the JVM handler, crashing the process. - // walkVM installs its own inner jmp_buf and chains back to whatever we set - // here, so nesting is safe. When there is no ProfiledThread we have nowhere - // to publish a jmp_buf, so we skip the unwind entirely rather than risk an - // unrecoverable fault on a raw dereference. - ProfiledThread *walk_thread = ProfiledThread::current(); - if (walk_thread == nullptr) { - num_frames += makeFrame(frames + num_frames, BCI_ERROR, "no_ProfiledThread"); - Counters::increment(SAMPLES_DROPPED_THREAD_LOCAL); - truncated = true; - } else { - sigjmp_buf unwind_ctx; - sigjmp_buf *prev_jmp_buf = walk_thread->getJmpCtx(); - int jmp_rc = sigsetjmp(unwind_ctx, 1); - if (jmp_rc != 0) { - // A fault during unwinding siglongjmp'd back here (via checkFault). The - // siglongjmp bypassed segvHandler's SignalHandlerScope destructor, so - // compensate, restore the previous sigjmp_buf chain, and record the - // partial trace with an error marker instead of crashing. - SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); - walk_thread->setJmpCtx(prev_jmp_buf); - truncated = true; - if (num_frames < _max_stack_depth) { - num_frames += makeFrame(frames + num_frames, BCI_ERROR, "break_unwind_fault"); - } - } else { - walk_thread->setJmpCtx(&unwind_ctx); - - // truncated_local is never read after a siglongjmp landing (only on the - // clean path below), so it need not be volatile; the outer `truncated` - // stays false on the recovery path. - bool truncated_local = false; - ASGCT_CallFrame *native_stop = frames + num_frames; - num_frames += getNativeTrace(ucontext, native_stop, event_type, tid, - &java_ctx, &truncated_local, lock_index); - assert(num_frames >= 0); - - int max_remaining = _max_stack_depth - num_frames; - if (max_remaining > 0) { - StackWalkRequest request = {event_type, lock_index, ucontext, frames + num_frames, max_remaining, &java_ctx, &truncated_local}; - num_frames += JVMSupport::walkJavaStack(request); - } - assert(num_frames >= 0); + int num_frames = 0; - walk_thread->setJmpCtx(prev_jmp_buf); - truncated = truncated_local; - } + StackContext java_ctx = {0}; + ASGCT_CallFrame *native_stop = frames + num_frames; + num_frames += getNativeTrace(ucontext, native_stop, event_type, tid, + &java_ctx, &truncated, lock_index); + assert(num_frames >= 0); + + int max_remaining = _max_stack_depth - num_frames; + if (max_remaining > 0) { + StackWalkRequest request = {event_type, lock_index, ucontext, frames + num_frames, max_remaining, &java_ctx, &truncated}; + num_frames += JVMSupport::walkJavaStack(request); } + + assert(num_frames >= 0); if (num_frames == 0) { - num_frames += makeFrame(frames + num_frames, BCI_ERROR, "no_Java_frame"); + num_frames += makeFrame(frames + num_frames, BCI_ERROR, "no_Java_frame"); } call_trace_id = @@ -1062,6 +1018,11 @@ int Profiler::crashHandlerInternal(int signo, siginfo_t *siginfo, void *ucontext if (VM::isHotspot()) { // the following checks require vmstructs and therefore HotSpot +(??) // HotspotSupport::checkFault has its own check if we're in a protected stack walk. +(??) // If the fault is from our protected walk, it will longjmp and never return. +(??) // If it returns, the fault wasn't from our code. +(??) HotspotSupport::checkFault(thrd); +(??) // Workaround for JDK-8313796 if needed. Setting cstack=dwarf also helps if (_need_JDK_8313796_workaround && VMStructs::isInterpretedFrameValidFunc((const void *)pc) && diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index 469236a455..3962ba2577 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -146,7 +146,7 @@ TEST_F(FaultInjectionTest, SafeAccessRecoversFromInjectedFault) { // (c2) walkVM path: a raw dereference of an injected poison pointer must be // caught by the sigsetjmp/siglongjmp crash protection, returning control to sigsetjmp. -TEST_F(FaultInjectionTest, WalkVmSetjmpRecoversFromInjectedFault) { +TEST_F(FaultInjectionTest, WalkVmSigsetjmpRecoversFromInjectedFault) { ProfiledThread* t = ProfiledThread::current(); ASSERT_NE(t, nullptr); @@ -183,90 +183,4 @@ TEST_F(FaultInjectionTest, WalkVmSetjmpRecoversFromInjectedFault) { SUCCEED(); } -// (c3) recordSample's outer sigsetjmp region (PROF-15447): Profiler::recordSample() -// wraps the native/Java unwind in its own sigjmp_buf, chaining off whatever -// context a caller may already have installed, so a fault in the -// *unprotected* metadata reads surrounding walkVM/walkFP/walkDwarf (isJitCode, -// findFrameDesc, findLibraryByAddress, AGCT in getJavaTraceAsync, frame.link, -// ...) recovers to a partial trace instead of crashing — and, critically, -// restores the caller's sigjmp_buf chain on both the clean and the recovery path. -// recordSample() itself needs a live JVM (ASGCT, VMStructs, an allocated -// _calltrace_buffer) unavailable in this gtest binary, so — following the -// same "replicate the protocol" approach used elsewhere in this suite for -// JVM-dependent code — this test reproduces its exact save/install/sigsetjmp/ -// siglongjmp/restore sequence verbatim, including the `volatile int num_frames` -// accumulator that must survive the siglongjmp landing, and drives it with the -// same probabilistic fault injection as WalkVmSetjmpRecoversFromInjectedFault. -// Unlike that test, this one also (a) starts from an already-installed -// "grandparent" jmp_buf to verify nesting/chain-restore, matching how -// recordSample can itself run nested under another sampler's protection, and -// (b) asserts STACKWALK_LONGJMP_RECOVERED actually increments, confirming the -// real Profiler::checkFault() (wired in via fi_signal_wrapper) did the -// recovery rather than some other signal-handling path. -TEST_F(FaultInjectionTest, RecordSampleOuterSetjmpRecoversAndRestoresChain) { - ProfiledThread* t = ProfiledThread::current(); - ASSERT_NE(t, nullptr); - - // Simulate a pre-existing outer protection context, as recordSample must - // support when nested inside another sampler's own protected region. - sigjmp_buf grandparent_ctx; - t->setJmpCtx(&grandparent_ctx); - uintptr_t real_slot = 0; // a valid, readable "metadata" slot - uintptr_t base = (uintptr_t)&real_slot; - long long recovered_before = Counters::getCounter(STACKWALK_LONGJMP_RECOVERED); - - // Read again after the sigsetjmp landing below, so it must be volatile — mirrors - // recordSample's own num_frames. - volatile int num_frames = 0; - // Frames "collected" before the fault, e.g. by a getNativeTrace() call that - // partially succeeded before walkJavaStack() faulted. Seeding a non-zero - // value here — mutated between sigsetjmp() and the siglongjmp below — is what - // actually exercises the volatile qualifier: a non-volatile local would be - // indeterminate after the jump, so the post-recovery checks below would not - // reliably see it. Without this seed, num_frames would still read 0 whether - // or not it were volatile, and the test would prove nothing about volatile. - constexpr int kSeededFrames = 3; - - jmp_buf unwind_ctx; - jmp_buf* prev_jmp_buf = t->getJmpCtx(); - ASSERT_EQ(&grandparent_ctx, prev_jmp_buf); - - t->setFiRng(0xC0FFEEC0FFEEC0FFULL); - - int jmp_rc = sigsetjmp(unwind_ctx, 1); - if (jmp_rc != 0) { - // Landed here via the real Profiler::checkFault() -> siglongjmp, triggered by - // an actual injected fault below. Mirrors recordSample's - // `if (num_frames < _max_stack_depth) { num_frames += makeFrame(...); }`: - // the recovery marker is appended to whatever partial progress survived - // the jump, never resets it. - t->setJmpCtx(prev_jmp_buf); - num_frames += 1; // stand-in for makeFrame(..., "break_unwind_fault") - } else { - t->setJmpCtx(&unwind_ctx); - num_frames = kSeededFrames; // mutated before the fault; must survive the siglongjmp - - // Force at least one fire deterministically, then let the tier drive the rest. - for (int i = 0; i < 5000 && num_frames == kSeededFrames; i++) { - // Raw deref of the (possibly poisoned) base — mirrors the unprotected - // metadata reads recordSample's outer sigsetjmp now guards. - uintptr_t v = *(uintptr_t*)INJECT_FAULT_ADDRESS_LIKELY(base); - asm volatile("" : "+r"(v) : : "memory"); - } - t->setJmpCtx(prev_jmp_buf); - } - - EXPECT_EQ(&grandparent_ctx, t->getJmpCtx()) - << "must restore the caller's jmp_buf chain on both the clean and the " - "recovery path, never leave it cleared or pointing at unwind_ctx"; - EXPECT_EQ(kSeededFrames + 1, num_frames) - << "the seeded pre-fault value must survive the siglongjmp landing and the " - "recovery marker must append to it, not overwrite it"; - EXPECT_GT(Counters::getCounter(STACKWALK_LONGJMP_RECOVERED), recovered_before) - << "checkFault() must have run and incremented the shared recovery counter"; - - t->setJmpCtx(nullptr); // leave the thread unprotected for subsequent tests -} - - #endif // __FAULT_INJECTION__ diff --git a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp index e9378ac7fd..b2054045db 100644 --- a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp +++ b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp @@ -16,7 +16,7 @@ * signal-based sampler interrupting a non-signal-based sampler's own * in-flight walkVM() call doesn't clobber the outer call's context. * 2. If a fault fires during the walk, checkFault() detects the live - * context via ProfiledThread::isProtected() and calls longjmp() to + * context via ProfiledThread::isProtected() and calls siglongjmp() to * unwind through whatever context is currently installed. * 3. ProfiledThread tracks nested crash-handler depth so recursive faults * (e.g. wall-clock signal inside a crash handler) are capped safely. @@ -115,7 +115,7 @@ TEST_F(ProfiledThreadTypeTest, FastPathReturnsFalseForNonJavaThread) { // Profiler::crashHandlerInternal calls: // enterCrashHandler() — on entry, returns false if limit reached // exitCrashHandler() — on normal exit -// resetCrashHandler() — from checkFault before longjmp to unwind all +// resetCrashHandler() — from checkFault before siglongjmp to unwind all // nesting at once // --------------------------------------------------------------------------- @@ -160,7 +160,7 @@ TEST_F(CrashHandlerNestingTest, LimitBlocksFurtherEntry) { } } -// resetCrashHandler() is called by checkFault() before longjmp so that the +// resetCrashHandler() is called by checkFault() before siglongjmp so that the // landing pad in walkVM starts with a clean nesting count. TEST_F(CrashHandlerNestingTest, ResetAllowsEntryAfterDeepNesting) { for (u32 i = 0; i < ProfiledThread::CRASH_HANDLER_NESTING_LIMIT; i++) { @@ -195,11 +195,11 @@ TEST_F(CrashHandlerNestingTest, IsDeepOnlyAboveLimit) { // A non-signal-based sampler's walkVM() call can itself be interrupted by a // signal-based sampler, putting two walkVM() frames on the same thread's // stack. Each frame follows the same protocol: -// sigjmp_buf* prev = prof_thread->getJmpCtx(); // save whatever was there +// sigjmp_buf* prev = prof_thread->getJmpCtx(); // save whatever was there // prof_thread->setJmpCtx(&my_ctx); // install this frame's ctx // ... walk ... // prof_thread->setJmpCtx(prev); // restore on every exit path -// checkFault() always longjmps through whatever is currently installed +// checkFault() always siglongjmps through whatever is currently installed // (thrd->getJmpCtx()), so the inner frame must never leave the outer frame's // context installed while the inner frame is doing its own protected work, // and must always hand it back — via normal completion or fault recovery — @@ -250,7 +250,7 @@ TEST_F(JmpCtxChainingTest, SingleFrameRestoresPreviousOnExit) { // Replicates two nested walkVM() calls: a signal-based sampler interrupting a // non-signal-based sampler's own in-flight walkVM(). The inner call must -// chain off the outer's jmp_buf*, install its own, and hand the outer's back +// chain off the outer's sigjmp_buf*, install its own, and hand the outer's back // on its way out — leaving the outer frame's context exactly as it left it. TEST_F(JmpCtxChainingTest, NestedFramesChainAndUnwindInOrder) { sigjmp_buf outer_ctx; @@ -306,7 +306,7 @@ TEST_F(JmpCtxChainingTest, FaultInInnerFrameDoesNotDisturbOuterFrame) { _pt->setJmpCtx(inner_prev); } else { _pt->setJmpCtx(&inner_ctx); - // Simulate checkFault(): longjmp through whatever is currently + // Simulate checkFault(): siglongjmp through whatever is currently // installed — this must hit the inner frame, not the outer. siglongjmp(*_pt->getJmpCtx(), 1); FAIL() << "unreachable: siglongjmp does not return"; From 67e8b04c22ad18cf713a52cdc29a73a065f754a3 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Tue, 28 Jul 2026 20:41:55 -0400 Subject: [PATCH 30/51] Fix merge --- ddprof-lib/src/main/cpp/profiler.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 8f6cf1f7dd..ea7d047716 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -680,6 +680,7 @@ bool Profiler::recordSample(void *ucontext, u64 counter, int tid, call_trace_id = _call_trace_storage.put(num_frames, frames, truncated, counter); + ProfiledThread* walk_thread = ProfiledThread::current(); if (walk_thread != nullptr) { walk_thread->recordCallTraceId(call_trace_id); } @@ -1018,11 +1019,6 @@ int Profiler::crashHandlerInternal(int signo, siginfo_t *siginfo, void *ucontext if (VM::isHotspot()) { // the following checks require vmstructs and therefore HotSpot -(??) // HotspotSupport::checkFault has its own check if we're in a protected stack walk. -(??) // If the fault is from our protected walk, it will longjmp and never return. -(??) // If it returns, the fault wasn't from our code. -(??) HotspotSupport::checkFault(thrd); -(??) // Workaround for JDK-8313796 if needed. Setting cstack=dwarf also helps if (_need_JDK_8313796_workaround && VMStructs::isInterpretedFrameValidFunc((const void *)pc) && From b99ba1bdf35f40630d4ca4855fae70a538dec1e9 Mon Sep 17 00:00:00 2001 From: "dd-octo-sts[bot]" <200755185+dd-octo-sts[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:41:09 +0200 Subject: [PATCH 31/51] [Automated] Bump dev version to 1.49.0 (#690) --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 1012048452..27889c255c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,7 +20,7 @@ plugins { id("com.datadoghq.native-root") } -version = "1.48.0-SNAPSHOT" +version = "1.49.0-SNAPSHOT" apply(plugin = "com.dipien.semantic-version") version = findProperty("ddprof_version") as? String ?: version From ad438066b0148cffb60a799d5e91135db0c2d097 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 29 Jul 2026 15:45:34 +0200 Subject: [PATCH 32/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/main/cpp/jvmSupport.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ddprof-lib/src/main/cpp/jvmSupport.h b/ddprof-lib/src/main/cpp/jvmSupport.h index e41a64dfdc..bcb96475f1 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.h +++ b/ddprof-lib/src/main/cpp/jvmSupport.h @@ -71,12 +71,14 @@ class JVMSupport { static inline bool isHidden(jint modifiers); }; +class ProfiledThread; + class LongjmpProtectionLeaver { private: - sigjmp_buf* _jmp_buf; + sigjmp_buf* _jmp_buf; ProfiledThread* const _thread; public: - LongjmpProtectionLeaver(ProfiledThread* const thread); + explicit LongjmpProtectionLeaver(ProfiledThread* thread); ~LongjmpProtectionLeaver(); }; From 4e5d0599def359c0f5f483083e3ae9625a59e069 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 29 Jul 2026 14:05:45 +0000 Subject: [PATCH 33/51] Protect HotspotSupport::walkJavaStack() --- .../src/main/cpp/hotspot/hotspotSupport.cpp | 55 +++++++++++----- .../src/main/cpp/hotspot/hotspotSupport.h | 1 - ddprof-lib/src/main/cpp/jvmSupport.cpp | 3 +- ddprof-lib/src/main/cpp/jvmSupport.h | 3 + ddprof-lib/src/main/cpp/jvmSupport.inline.h | 6 ++ ddprof-lib/src/main/cpp/stackWalker.cpp | 62 ++++++++++++++++++- 6 files changed, 110 insertions(+), 20 deletions(-) diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index 6a926906cc..d6c6c6c628 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -978,11 +978,6 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex return depth; } -void HotspotSupport::JVMAsyncGetCallTrace(ASGCT_CallTrace* traces, jint depth, void* ucontext) { - LongjmpProtectionLeaver leaver(ProfiledThread::current()); - VM::_asyncGetCallTrace(traces, depth, ucontext); -} - int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, int max_depth, StackContext *java_ctx, bool *truncated) { @@ -1060,7 +1055,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, JitWriteProtection jit(false); // AsyncGetCallTrace writes to ASGCT_CallFrame array ASGCT_CallTrace trace = {jni, 0, frames}; - JVMAsyncGetCallTrace(&trace, max_depth, ucontext); + JVMSupport::jvmAsyncGetCallTrace(&trace, max_depth, ucontext); if (trace.num_frames > 0) { frame.restore(saved_pc, saved_sp, saved_fp); @@ -1081,7 +1076,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, if (!(safe_mode & POP_STUB) && frame.unwindStub((instruction_t *)stub->_start, stub->_name) && isAddressInCode((const void *)frame.pc())) { - JVMAsyncGetCallTrace(&trace, max_depth, ucontext); + JVMSupport::jvmAsyncGetCallTrace(&trace, max_depth, ucontext); } } else if (VMStructs::hasMethodStructs()) { VMNMethod *nmethod = CodeHeap::findNMethod((const void *)frame.pc()); @@ -1094,7 +1089,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, } if (!(safe_mode & POP_METHOD) && frame.unwindCompiled(nmethod) && isAddressInCode((const void *)frame.pc())) { - JVMAsyncGetCallTrace(&trace, max_depth, ucontext); + JVMSupport::jvmAsyncGetCallTrace(&trace, max_depth, ucontext); } if ((safe_mode & PROBE_SP) && trace.num_frames < 0) { if (isValidJMethodID(method_id)) { @@ -1102,7 +1097,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, } for (int i = 0; trace.num_frames < 0 && i < PROBE_SP_LIMIT; i++) { frame.sp() += sizeof(void*); - JVMAsyncGetCallTrace(&trace, max_depth, ucontext); + JVMSupport::jvmAsyncGetCallTrace(&trace, max_depth, ucontext); } } } @@ -1114,7 +1109,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, if (!(safe_mode & POP_STUB) && frame.unwindStub(NULL, nmethod->name()) && isAddressInCode((const void *)frame.pc())) { - JVMAsyncGetCallTrace(&trace, max_depth, ucontext); + JVMSupport::jvmAsyncGetCallTrace(&trace, max_depth, ucontext); } } } @@ -1141,9 +1136,9 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, m->frameCompleteOffset() == -1) { m->setFrameCompleteOffset(0); } - JVMAsyncGetCallTrace(&trace, max_depth, ucontext); + JVMSupport::jvmAsyncGetCallTrace(&trace, max_depth, ucontext); } else if (libs->findLibraryByAddress(pc) != NULL) { - JVMAsyncGetCallTrace(&trace, max_depth, ucontext); + JVMSupport::jvmAsyncGetCallTrace(&trace, max_depth, ucontext); } anchor->setLastJavaPC(nullptr); @@ -1161,7 +1156,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, if (m != NULL && !m->isNMethod() && m->frameSize() > 0 && m->frameCompleteOffset() == -1) { m->setFrameCompleteOffset(0); - JVMAsyncGetCallTrace(&trace, max_depth, ucontext); + JVMSupport::jvmAsyncGetCallTrace(&trace, max_depth, ucontext); } } } else if (trace.num_frames == ticks_GC_active && !(safe_mode & GC_TRACES)) { @@ -1202,7 +1197,33 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { bool* truncated = request.truncated; u32 lock_index = request.lock_index; - int java_frames = 0; + volatile int java_frames = 0; + + // walkVM() installs its own sigsetjmp/siglongjmp crash protection (chained + // with any pre-existing jmp ctx, see the comment in walkVM), but the + // getJavaTraceAsync() path below runs without one: it dereferences + // VMThread/anchor state directly and calls into HotSpot's own + // AsyncGetCallTrace. Install a jmp ctx here too, so a SIGSEGV anywhere in + // walkJavaStack is caught by Profiler::checkFault() and siglongjmp'd back + // here instead of crashing the process. + ProfiledThread* prof_thread = ProfiledThread::current(); + sigjmp_buf crash_protection_ctx; + sigjmp_buf* prev_jmp_buf = prof_thread != nullptr ? prof_thread->getJmpCtx() : nullptr; + + if (prof_thread != nullptr && sigsetjmp(crash_protection_ctx, 1) != 0) { + // checkFault() does a siglongjmp from inside segvHandler, bypassing + // segvHandler's SignalHandlerScope destructor. Compensate. + SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); + prof_thread->setJmpCtx(prev_jmp_buf); + if (truncated) { + *truncated = true; + } + return java_frames; + } + if (prof_thread != nullptr) { + prof_thread->setJmpCtx(&crash_protection_ctx); + } + if (features.mixed) { java_frames = walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated); } else if (isHookPrefixedSample(request.event_type)) { @@ -1257,7 +1278,11 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { } } } - return java_frames; + + if (prof_thread != nullptr) { + prof_thread->setJmpCtx(prev_jmp_buf); + } + return java_frames; } static void patchClassLoaderData(JNIEnv* jni, jclass klass) { diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h index 65e0edf9da..6aed4ff00d 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h @@ -31,7 +31,6 @@ class HotspotSupport { static int getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames, int max_depth, StackContext *java_ctx, bool *truncated); - static void JVMAsyncGetCallTrace(ASGCT_CallTrace *, jint, void *); static bool loadMethodIDsIfNeededImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass, bool load_all); public: diff --git a/ddprof-lib/src/main/cpp/jvmSupport.cpp b/ddprof-lib/src/main/cpp/jvmSupport.cpp index 7612985982..f72a3f319d 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.cpp +++ b/ddprof-lib/src/main/cpp/jvmSupport.cpp @@ -106,12 +106,11 @@ int JVMSupport::asyncGetCallTrace(ASGCT_CallFrame *frames, int max_depth, void* return 0; } - LongjmpProtectionLeaver leaver(ProfiledThread::current()); JitWriteProtection jit(false); // AsyncGetCallTrace writes to ASGCT_CallFrame array ASGCT_CallTrace trace = {jni, 0, frames}; - VM::_asyncGetCallTrace(&trace, max_depth, ucontext); + jvmAsyncGetCallTrace(&trace, max_depth, ucontext); if (trace.num_frames > 0) { return trace.num_frames; } diff --git a/ddprof-lib/src/main/cpp/jvmSupport.h b/ddprof-lib/src/main/cpp/jvmSupport.h index e41a64dfdc..71d907a906 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.h +++ b/ddprof-lib/src/main/cpp/jvmSupport.h @@ -37,6 +37,9 @@ class JVMSupport { static Mutex _initialization_lock; static volatile JMethodIDLoadStats jmethodID_load_state; + // Call JVM AsyncGetCallTrace implementation + static inline void jvmAsyncGetCallTrace(ASGCT_CallTrace *frames, int max_depth, void* ucontext); + static int asyncGetCallTrace(ASGCT_CallFrame *frames, int max_depth, void* ucontext); // J9 and Zing shared implementation, load jmethodIDs of the method unconditionally. static bool loadMethodIDsImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass); diff --git a/ddprof-lib/src/main/cpp/jvmSupport.inline.h b/ddprof-lib/src/main/cpp/jvmSupport.inline.h index 3be11d5902..76d206d259 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.inline.h +++ b/ddprof-lib/src/main/cpp/jvmSupport.inline.h @@ -8,6 +8,7 @@ #include "hotspot/hotspotSupport.h" #include "jvmSupport.h" +#include "threadLocalData.h" #include "vmEntry.h" bool JVMSupport::canUnwind(const StackFrame& frame, const void*& pc) { @@ -53,4 +54,9 @@ bool JVMSupport::isHidden(jint modifiers) { ((modifiers & hidden_mask) != 0); } +void JVMSupport::jvmAsyncGetCallTrace(ASGCT_CallTrace *frames, int max_depth, void* ucontext) { + LongjmpProtectionLeaver leaver(ProfiledThread::current()); + VM::_asyncGetCallTrace(frames, max_depth, ucontext); +} + #endif // _JVMSUPPORT_INLINE_H diff --git a/ddprof-lib/src/main/cpp/stackWalker.cpp b/ddprof-lib/src/main/cpp/stackWalker.cpp index 48255415fe..4de65b5805 100644 --- a/ddprof-lib/src/main/cpp/stackWalker.cpp +++ b/ddprof-lib/src/main/cpp/stackWalker.cpp @@ -38,9 +38,34 @@ int StackWalker::walkFP(void* ucontext, const void** callchain, int max_depth, S sp = frame.sp(); } - int depth = 0; + volatile int depth = 0; int actual_max_depth = truncated ? max_depth + 1 : max_depth; + // Mirrors HotspotSupport::walkVM's crash protection: a SIGSEGV whose PC + // falls inside this library while a jmp ctx is installed gets caught by + // Profiler::checkFault() from the SEGV handler and siglongjmp'd back here, + // instead of crashing the process. + ProfiledThread* prof_thread = ProfiledThread::current(); + sigjmp_buf crash_protection_ctx; + sigjmp_buf* prev_jmp_buf = prof_thread != nullptr ? prof_thread->getJmpCtx() : nullptr; + + if (prof_thread != nullptr && sigsetjmp(crash_protection_ctx, 1) != 0) { + // checkFault() does a siglongjmp from inside segvHandler, bypassing + // segvHandler's SignalHandlerScope destructor. Compensate. + SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); + prof_thread->setJmpCtx(prev_jmp_buf); + if (truncated) { + *truncated = true; + if (depth > max_depth) { + depth = max_depth; + } + } + return depth; + } + if (prof_thread != nullptr) { + prof_thread->setJmpCtx(&crash_protection_ctx); + } + // Walk until the bottom of the stack or until the first Java frame while (depth < actual_max_depth) { if (JVMSupport::isJitCode(pc) && !(depth == 0 && JVMSupport::canUnwind(frame, pc)) && @@ -70,6 +95,10 @@ int StackWalker::walkFP(void* ucontext, const void** callchain, int max_depth, S fp = (uintptr_t)SafeAccess::load(INJECT_FAULT_ADDRESS_LIKELY((void**)fp)); } + if (prof_thread != nullptr) { + prof_thread->setJmpCtx(prev_jmp_buf); + } + if (truncated && depth > max_depth) { *truncated = true; depth = max_depth; @@ -95,10 +124,35 @@ int StackWalker::walkDwarf(void* ucontext, const void** callchain, int max_depth sp = frame.sp(); } - int depth = 0; + volatile int depth = 0; Profiler* profiler = Profiler::instance(); int actual_max_depth = truncated ? max_depth + 1 : max_depth; + // Mirrors HotspotSupport::walkVM's crash protection: a SIGSEGV whose PC + // falls inside this library while a jmp ctx is installed gets caught by + // Profiler::checkFault() from the SEGV handler and siglongjmp'd back here, + // instead of crashing the process. + ProfiledThread* prof_thread = ProfiledThread::current(); + sigjmp_buf crash_protection_ctx; + sigjmp_buf* prev_jmp_buf = prof_thread != nullptr ? prof_thread->getJmpCtx() : nullptr; + + if (prof_thread != nullptr && sigsetjmp(crash_protection_ctx, 1) != 0) { + // checkFault() does a siglongjmp from inside segvHandler, bypassing + // segvHandler's SignalHandlerScope destructor. Compensate. + SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); + prof_thread->setJmpCtx(prev_jmp_buf); + if (truncated) { + *truncated = true; + if (depth > max_depth) { + depth = max_depth; + } + } + return depth; + } + if (prof_thread != nullptr) { + prof_thread->setJmpCtx(&crash_protection_ctx); + } + // Walk until the bottom of the stack or until the first Java frame while (depth < actual_max_depth) { if (JVMSupport::isJitCode(pc) && !(depth == 0 && JVMSupport::canUnwind(frame, pc)) && @@ -175,6 +229,10 @@ int StackWalker::walkDwarf(void* ucontext, const void** callchain, int max_depth } } + if (prof_thread != nullptr) { + prof_thread->setJmpCtx(prev_jmp_buf); + } + if (truncated && depth > max_depth) { *truncated = true; depth = max_depth; From 040baede74f32a59fc8257e253590ef50ab43762 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 29 Jul 2026 15:59:50 +0000 Subject: [PATCH 34/51] Cleanup --- ddprof-lib/src/main/cpp/faultInjection.h | 5 +++++ ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp | 6 +++--- ddprof-lib/src/main/cpp/jvmSupport.cpp | 14 -------------- ddprof-lib/src/main/cpp/jvmSupport.h | 11 ----------- ddprof-lib/src/main/cpp/jvmSupport.inline.h | 1 - ddprof-lib/src/main/cpp/profiler.cpp | 11 +++++------ 6 files changed, 13 insertions(+), 35 deletions(-) diff --git a/ddprof-lib/src/main/cpp/faultInjection.h b/ddprof-lib/src/main/cpp/faultInjection.h index a88dbe5c41..5ac8ead3ba 100644 --- a/ddprof-lib/src/main/cpp/faultInjection.h +++ b/ddprof-lib/src/main/cpp/faultInjection.h @@ -40,11 +40,14 @@ #ifndef _FAULT_INJECTION_H #define _FAULT_INJECTION_H +#include + #ifdef __FAULT_INJECTION__ #include "arch.h" // u64 #include +#define NO_INJECTION_ASSERT(a) namespace faultinj { // Firing probability expressed as an xorshift64 threshold (round(p * 2^64)), so @@ -129,6 +132,8 @@ inline T injectValue(T orig, T faulty, u64 threshold, const char* fn) { #define INJECT_FAULT_BOOL_UNLIKELY(v) (v) #define INJECT_FAULT_BOOL_LIKELY(v) (v) +#define NO_INJECTION_ASSERT(a) (assert(a)) + #endif // __FAULT_INJECTION__ #endif // _FAULT_INJECTION_H diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index d6c6c6c628..9cf50abc2f 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -186,7 +186,7 @@ static void fillFrameTypes(ASGCT_CallFrame *frames, int num_frames, VMNMethod *n // Fill the frame with raw method pointer static void fillFrameRaw(ASGCT_CallFrame& frame, FrameTypeId type, int bci, const VMMethod* method) { - assert(method != nullptr); + NO_INJECTION_ASSERT(method != nullptr); frame.bci = FrameType::encode(type, bci, true /*raw method pointer*/); frame.method = static_cast(method); } @@ -196,7 +196,7 @@ static void fillFrame(ASGCT_CallFrame& frame, FrameTypeId type, int bci, jmethod if (method_id != nullptr && method_id != JMETHODID_NOT_WALKABLE) { fillFrame(frame, type, bci, method_id); } else { - assert(method != nullptr); + NO_INJECTION_ASSERT(method != nullptr); fillFrameRaw(frame, type, bci, method); } } @@ -1389,7 +1389,7 @@ bool HotspotSupport::loadMethodIDsIfNeededImpl(jvmtiEnv *jvmti, JNIEnv *jni, jcl // This method only resolves methods that are loaded by system class loaders jmethodID HotspotSupport::resolve(const void* method) { assert(VM::isHotspot()); - assert(method != nullptr); + NO_INJECTION_ASSERT(method != nullptr); // We packed not walkable method as a raw pointer, // map it back to nullptr, as JMETHODID_NOT_WALKABLE is only // known in hotspot. diff --git a/ddprof-lib/src/main/cpp/jvmSupport.cpp b/ddprof-lib/src/main/cpp/jvmSupport.cpp index f72a3f319d..d05d8a641e 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.cpp +++ b/ddprof-lib/src/main/cpp/jvmSupport.cpp @@ -177,17 +177,3 @@ bool JVMSupport::loadMethodIDsImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass) { } return false; } - -LongjmpProtectionLeaver::LongjmpProtectionLeaver(ProfiledThread* const thread) : - _thread(thread), _jmp_buf(nullptr) { - if (thread != nullptr) { - _jmp_buf = thread->getJmpCtx(); - thread->setJmpCtx(nullptr); - } -} - -LongjmpProtectionLeaver::~LongjmpProtectionLeaver() { - if (_thread != nullptr) { - _thread->setJmpCtx(_jmp_buf); - } -} diff --git a/ddprof-lib/src/main/cpp/jvmSupport.h b/ddprof-lib/src/main/cpp/jvmSupport.h index 1dfdafeaae..8c9065adc3 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.h +++ b/ddprof-lib/src/main/cpp/jvmSupport.h @@ -74,15 +74,4 @@ class JVMSupport { static inline bool isHidden(jint modifiers); }; -class ProfiledThread; - -class LongjmpProtectionLeaver { -private: - sigjmp_buf* _jmp_buf; - ProfiledThread* const _thread; -public: - explicit LongjmpProtectionLeaver(ProfiledThread* thread); - ~LongjmpProtectionLeaver(); -}; - #endif // _JVMSUPPORT_H diff --git a/ddprof-lib/src/main/cpp/jvmSupport.inline.h b/ddprof-lib/src/main/cpp/jvmSupport.inline.h index 76d206d259..bea61880ff 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.inline.h +++ b/ddprof-lib/src/main/cpp/jvmSupport.inline.h @@ -55,7 +55,6 @@ bool JVMSupport::isHidden(jint modifiers) { } void JVMSupport::jvmAsyncGetCallTrace(ASGCT_CallTrace *frames, int max_depth, void* ucontext) { - LongjmpProtectionLeaver leaver(ProfiledThread::current()); VM::_asyncGetCallTrace(frames, max_depth, ucontext); } diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index aa2373656a..a88e33c7f2 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -2004,7 +2004,7 @@ int Profiler::status(char* status, int max_len) { } void Profiler::checkFault(ProfiledThread* thrd, siginfo_t *siginfo, void *ucontext) { - (void)ucontext; + (void)siginfo; // Check if siglongjmp is setup for this thread if (thrd == nullptr || !thrd->isProtected()) { return; @@ -2012,15 +2012,14 @@ void Profiler::checkFault(ProfiledThread* thrd, siginfo_t *siginfo, void *uconte // Check if the fault is originated from java profiler const uintptr_t pc = (uintptr_t)StackFrame(ucontext).pc(); - const uintptr_t min = (uintptr_t)profiler_min_address; - const uintptr_t max = (uintptr_t)profiler_max_address; + const uintptr_t min = profiler_min_address.load(std::memory_order_relaxed); + const uintptr_t max = profiler_max_address.load(std::memory_order_relaxed); + // If the profiler address range is not initialized (e.g. unit tests), fall back + // to recovering unconditionally when a protection context is installed. #if !defined(UNIT_TEST) assert(min != 0 && max != 0); #endif - - // If the profiler address range is not initialized (e.g. unit tests), fall back - // to recovering unconditionally when a protection context is installed. if ((min != 0 && max != 0) && (pc < min || pc >= max)) { return; } From 94c4cbd6419385bf99931defb9814801a19ca5e8 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 29 Jul 2026 16:13:13 +0000 Subject: [PATCH 35/51] Fix compilation --- ddprof-lib/src/main/cpp/jvmSupport.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddprof-lib/src/main/cpp/jvmSupport.cpp b/ddprof-lib/src/main/cpp/jvmSupport.cpp index d05d8a641e..b6277a6be4 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.cpp +++ b/ddprof-lib/src/main/cpp/jvmSupport.cpp @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "jvmSupport.h" +#include "jvmSupport.inline.h" #include "asyncSampleMutex.h" #include "frames.h" From d8d7b36307656dd42e05f92bfc00f5886ac8167c Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 29 Jul 2026 17:45:23 +0000 Subject: [PATCH 36/51] Fix test --- ddprof-lib/src/main/cpp/profiler.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ddprof-lib/src/main/cpp/profiler.h b/ddprof-lib/src/main/cpp/profiler.h index 6c52fbe1d1..5b10c133f2 100644 --- a/ddprof-lib/src/main/cpp/profiler.h +++ b/ddprof-lib/src/main/cpp/profiler.h @@ -498,6 +498,15 @@ class alignas(alignof(SpinLock)) Profiler { // Keep backward compatibility with the upstream async-profiler inline CodeCache* findLibraryByAddress(const void *address) { +#ifdef DEBUG + // we need this code to simulate segfault during stackwalking + // this is a safe place to do it since this wrapper is used solely from the 'vm' stackwalker implementation + if (force_stackwalk_crash_env) { + TEST_LOG("FORCE_SIGSEGV"); + int* p = nullptr; + *p = 1; + } +#endif return Libraries::instance()->findLibraryByAddress(address); } From ed3cfa58c3e7c0d2f31f420b8a61034fe08bc0d3 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 29 Jul 2026 19:18:51 +0000 Subject: [PATCH 37/51] Fix --- .../src/main/cpp/hotspot/hotspotSupport.cpp | 15 ++++++++++ ddprof-lib/src/main/cpp/profiler.cpp | 30 ++++++++++--------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index 9cf50abc2f..e4bc9d0b75 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -1198,6 +1198,14 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { u32 lock_index = request.lock_index; volatile int java_frames = 0; + // True exactly while an AsyncSampleMutex acquired by this call is alive, + // i.e. while ProfiledThread::is_unwinding_Java() is held on our behalf. + // A siglongjmp out of the getJavaTraceAsync() branches below bypasses that + // mutex's destructor, so the recovery path below uses this flag to release + // the per-thread guard itself — otherwise it would stay stuck true forever + // and permanently disable async CPU/wall/malloc/socket sampling on this + // thread. + volatile bool async_trace_active = false; // walkVM() installs its own sigsetjmp/siglongjmp crash protection (chained // with any pre-existing jmp ctx, see the comment in walkVM), but the @@ -1215,6 +1223,9 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { // segvHandler's SignalHandlerScope destructor. Compensate. SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); prof_thread->setJmpCtx(prev_jmp_buf); + if (async_trace_active) { + prof_thread->set_unwinding_Java(false); + } if (truncated) { *truncated = true; } @@ -1232,6 +1243,7 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { } else { AsyncSampleMutex mutex(ProfiledThread::current()); if (mutex.acquired()) { + async_trace_active = true; java_frames = getJavaTraceAsync(ucontext, frames, max_depth, java_ctx, truncated); if (java_frames > 0 && java_ctx->pc != NULL && VMStructs::hasMethodStructs()) { VMNMethod* nmethod = CodeHeap::findNMethod(java_ctx->pc); @@ -1239,6 +1251,7 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { fillFrameTypes(frames, java_frames, nmethod); } } + async_trace_active = false; } if (java_frames > 0 && VM::hotspot_version() >= 21 && java_frames < max_depth) { VMThread* carrier = VMThread::current(); @@ -1257,6 +1270,7 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { // Async events AsyncSampleMutex mutex(ProfiledThread::current()); if (mutex.acquired()) { + async_trace_active = true; java_frames = getJavaTraceAsync(ucontext, frames, max_depth, java_ctx, truncated); if (java_frames > 0 && java_ctx->pc != NULL && VMStructs::hasMethodStructs()) { VMNMethod* nmethod = CodeHeap::findNMethod(java_ctx->pc); @@ -1264,6 +1278,7 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { fillFrameTypes(frames, java_frames, nmethod); } } + async_trace_active = false; } // ASGCT stops at the continuation boundary for virtual threads (JDK 21+). // Append a synthetic root frame so the UI does not show "Missing Frames". diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index a88e33c7f2..1196db6b17 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -1048,17 +1048,32 @@ static std::atomic profiler_max_address{0}; void Profiler::setupSignalHandlers() { // Do not re-run the signal setup (run only when VM has not been loaded yet) if (__sync_bool_compare_and_swap(&_signals_initialized, false, true)) { + // Initialize infrastructure before enabling signal handler + // Eagerly initialize the Counters singleton off the signal path, before any // handler that increments counters is installed. The crash handler // (crashHandlerInternal -> SafeAccess::handle_safefetch) bumps // SAFEFETCH_FAILED / SAFECOPY_FAILED, and other async handlers bump the - // WALKVM_* counters. The first touch of the singleton lazily runs + // STACKWALK* counters. The first touch of the singleton lazily runs // aligned_alloc + memset and takes the C++ static-init guard lock — none of // which are async-signal-safe. Forcing that construction here guarantees the // signal path only ever performs lock-free atomic increments on the // already-allocated array. (void)Counters::getCounters(); + // Get address range of java profiler library + Libraries* libs = Libraries::instance(); + CodeCache* prof_lib = libs->findLibraryByAddress((const void*)&Profiler::setupSignalHandlers); + assert(prof_lib != nullptr); + profiler_min_address = reinterpret_cast(prof_lib->minAddress()); + profiler_max_address = reinterpret_cast(prof_lib->maxAddress()); + + #ifdef __FAULT_INJECTION__ + // Reserve the PROT_NONE guard region used to poison memory-access sites. + // Done here (off the signal path) once handlers are installed. + faultinj::init(); + #endif + if (VM::isHotspot() || VM::isOpenJ9()) { // HotSpot and J9 tolerate interposed SIGSEGV/SIGBUS handler; other JVMs probably not // IMPORTANT: protectSignalHandlers must be called BEFORE replaceSigsegvHandler so that @@ -1071,19 +1086,6 @@ void Profiler::setupSignalHandlers() { // Patch sigaction GOT in libraries with broken signal handlers (already loaded) LibraryPatcher::patch_sigaction(); } - - // Get address range of java profiler library - Libraries* libs = Libraries::instance(); - CodeCache* prof_lib = libs->findLibraryByAddress((const void*)&Profiler::setupSignalHandlers); - assert(prof_lib != nullptr); - profiler_min_address = reinterpret_cast(prof_lib->minAddress()); - profiler_max_address = reinterpret_cast(prof_lib->maxAddress()); - - #ifdef __FAULT_INJECTION__ - // Reserve the PROT_NONE guard region used to poison memory-access sites. - // Done here (off the signal path) once handlers are installed. - faultinj::init(); -#endif } } From 9e8d45fae773d4c991ccc75d4fcd1b1782156878 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 29 Jul 2026 20:12:05 +0000 Subject: [PATCH 38/51] Add tests --- .../test/cpp/hotspot_crash_protection_ut.cpp | 118 ++++++++++++++++++ ddprof-lib/src/test/cpp/stackWalker_ut.cpp | 90 +++++++++++++ 2 files changed, 208 insertions(+) diff --git a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp index b2054045db..e025f8ee6f 100644 --- a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp +++ b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp @@ -25,12 +25,15 @@ * A. ProfiledThread thread-type classification (isJavaThread fast path) * B. Crash-handler nesting depth (ProfiledThread crash handler state) * C. sigjmp_buf chaining across nested/interrupted walkVM() calls + * F. HotspotSupport::walkJavaStack()'s AsyncSampleMutex release on a + * recovered fault */ #include #include "threadLocalData.h" #include "profiler.h" +#include "asyncSampleMutex.h" #include "jvmThread.h" #include "safeAccess.h" #include "os.h" @@ -415,4 +418,119 @@ TEST_F(SafeFetch64TocTouGuardTest, ZeroReturnMeansGiveUp) { munmap(page, 4096); } +// --------------------------------------------------------------------------- +// F. HotspotSupport::walkJavaStack()'s AsyncSampleMutex release on a +// recovered fault +// +// walkJavaStack() wraps its getJavaTraceAsync() branches in an +// AsyncSampleMutex, which sets ProfiledThread::is_unwinding_Java() true for +// as long as it is alive and clears it again in its destructor. A siglongjmp +// out of that region (Profiler::checkFault() recovering a fault) bypasses +// the destructor, so walkJavaStack tracks a local `async_trace_active` flag +// that mirrors the mutex's true lifetime and, on recovery, explicitly clears +// is_unwinding_Java() itself whenever that flag was set — otherwise the flag +// would stay stuck true forever and permanently disable async CPU/wall/ +// malloc/socket sampling on that thread (see hotspotSupport.cpp). +// +// This gtest binary has no live JVM, so getJavaTraceAsync() itself can't be +// driven into a real fault — it bails out early on a null VMThread. These +// tests replicate walkJavaStack's exact protection/AsyncSampleMutex protocol +// with a real ProfiledThread, a real AsyncSampleMutex and a real +// sigsetjmp/siglongjmp fault (via the real Profiler::segvHandler -> +// Profiler::checkFault chain) to lock down the release behavior. +// --------------------------------------------------------------------------- + +class WalkJavaStackAsyncMutexRecoveryTest : public ::testing::Test { +protected: + void SetUp() override { + ProfiledThread::initCurrentThread(); + _pt = ProfiledThread::current(); + ASSERT_NE(nullptr, _pt); + ASSERT_FALSE(_pt->isProtected()); + ASSERT_FALSE(_pt->is_unwinding_Java()); + + _orig_segv = OS::replaceSigsegvHandler(Profiler::segvHandler); + _orig_bus = OS::replaceSigbusHandler(Profiler::busHandler); + + _bad_page = mmap(nullptr, 4096, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + ASSERT_NE(MAP_FAILED, _bad_page); + } + + void TearDown() override { + munmap(_bad_page, 4096); + OS::replaceSigsegvHandler(_orig_segv); + OS::replaceSigbusHandler(_orig_bus); + _pt->set_unwinding_Java(false); // keep failures from this test isolated + ProfiledThread::release(); + } + + ProfiledThread* _pt = nullptr; + void* _bad_page = nullptr; + SigAction _orig_segv = nullptr; + SigAction _orig_bus = nullptr; +}; + +// Replicates walkJavaStack's cstackgetJmpCtx(); + ASSERT_EQ(nullptr, prev_jmp_buf); + + volatile bool async_trace_active = false; + + if (sigsetjmp(crash_protection_ctx, 1) != 0) { + SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); + _pt->setJmpCtx(prev_jmp_buf); + if (async_trace_active) { + _pt->set_unwinding_Java(false); + } + } else { + _pt->setJmpCtx(&crash_protection_ctx); + + AsyncSampleMutex mutex(_pt); + ASSERT_TRUE(mutex.acquired()); + async_trace_active = true; + EXPECT_TRUE(_pt->is_unwinding_Java()); + + // Simulate a fault inside getJavaTraceAsync's raw memory access -- + // this never returns; it lands back at the sigsetjmp above via + // Profiler::checkFault()'s siglongjmp. + *reinterpret_cast(_bad_page) = 1; + FAIL() << "unreachable: the write above must fault"; + } + + EXPECT_FALSE(_pt->is_unwinding_Java()) + << "a recovered fault must release the AsyncSampleMutex guard, not leak it"; + EXPECT_FALSE(_pt->isProtected()); +} + +// Baseline: a fault that occurs *outside* the AsyncSampleMutex's window +// (async_trace_active still false, as in walkVM()'s own branches) must not +// touch is_unwinding_Java at all -- confirms the recovery reset is scoped to +// the mutex's true lifetime rather than firing unconditionally. +TEST_F(WalkJavaStackAsyncMutexRecoveryTest, RecoveredFaultOutsideGuardWindowLeavesFlagUntouched) { + sigjmp_buf crash_protection_ctx; + sigjmp_buf* prev_jmp_buf = _pt->getJmpCtx(); + volatile bool async_trace_active = false; + + if (sigsetjmp(crash_protection_ctx, 1) != 0) { + SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); + _pt->setJmpCtx(prev_jmp_buf); + if (async_trace_active) { + _pt->set_unwinding_Java(false); + } + } else { + _pt->setJmpCtx(&crash_protection_ctx); + + // walkVM()-style branch: no AsyncSampleMutex involved at all. + *reinterpret_cast(_bad_page) = 1; + FAIL() << "unreachable: the write above must fault"; + } + + EXPECT_FALSE(_pt->is_unwinding_Java()); + EXPECT_FALSE(_pt->isProtected()); +} + #endif // __linux__ diff --git a/ddprof-lib/src/test/cpp/stackWalker_ut.cpp b/ddprof-lib/src/test/cpp/stackWalker_ut.cpp index b0230bd2a7..5bc4536c37 100644 --- a/ddprof-lib/src/test/cpp/stackWalker_ut.cpp +++ b/ddprof-lib/src/test/cpp/stackWalker_ut.cpp @@ -6,6 +6,13 @@ #include "../../main/cpp/stackWalker.h" #include "../../main/cpp/gtest_crash_handler.h" +#ifdef __linux__ +#include +#include "../../main/cpp/os.h" +#include "../../main/cpp/profiler.h" +#include "../../main/cpp/threadLocalData.h" +#endif + static constexpr char STACKWALKER_TEST_NAME[] = "StackWalkerTest"; class StackWalkerTest : public ::testing::Test { @@ -141,3 +148,86 @@ TEST_F(StackWalkerTest, isValidSP_valid_aligned_in_range) { EXPECT_TRUE(StackWalkValidation::isValidSP(lo + 8, lo, hi)); EXPECT_TRUE(StackWalkValidation::isValidSP(hi - 8, lo, hi)); } + +#ifdef __linux__ + +// --------------------------------------------------------------------------- +// Crash recovery: StackWalker::walkFP() / walkDwarf() install a +// sigsetjmp/siglongjmp jmp ctx (mirroring HotspotSupport::walkVM's, see +// stackWalker.cpp) so a SIGSEGV anywhere in the walk is recovered by +// Profiler::checkFault() instead of crashing the process. +// +// `callchain[depth++] = pc;` is the one write in each walker that is NOT +// routed through SafeAccess::load — passing a PROT_NONE `callchain` buffer +// faults on that very first store, before any frame-pointer chasing, so it +// can only be recovered by this jmp-ctx protection (not by safefetch). +// +// These tests install the real Profiler::segvHandler/busHandler (the actual +// production chain: safefetch check, then Profiler::checkFault()) and call +// the real StackWalker functions directly — no live JVM is needed since +// JVMSupport::isJitCode()/JVMThread::current() both degrade to safe +// not-a-JVM-thread defaults without one. +// --------------------------------------------------------------------------- + +class StackWalkerCrashRecoveryTest : public ::testing::Test { +protected: + void SetUp() override { + ProfiledThread::initCurrentThread(); + _pt = ProfiledThread::current(); + ASSERT_NE(nullptr, _pt); + ASSERT_FALSE(_pt->isProtected()); + + _orig_segv = OS::replaceSigsegvHandler(Profiler::segvHandler); + _orig_bus = OS::replaceSigbusHandler(Profiler::busHandler); + + _bad_page = mmap(nullptr, 4096, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + ASSERT_NE(MAP_FAILED, _bad_page); + } + + void TearDown() override { + munmap(_bad_page, 4096); + OS::replaceSigsegvHandler(_orig_segv); + OS::replaceSigbusHandler(_orig_bus); + ProfiledThread::release(); + } + + ProfiledThread* _pt = nullptr; + void* _bad_page = nullptr; + SigAction _orig_segv = nullptr; + SigAction _orig_bus = nullptr; +}; + +TEST_F(StackWalkerCrashRecoveryTest, WalkFPRecoversFromFaultInsteadOfCrashing) { + StackContext java_ctx{}; + bool truncated = false; + const void** callchain = reinterpret_cast(_bad_page); + + // ucontext = nullptr makes walkFP() start from this call's own real + // pc/fp/sp (callerPC/FP/SP) -- the walk is genuinely live, it just can't + // write its first frame into the unmapped callchain buffer. + int depth = StackWalker::walkFP(nullptr, callchain, 64, &java_ctx, &truncated); + + // The exact depth at the moment of the fault is compiler-dependent (the + // `depth++` store may or may not have landed before the faulting write), + // so only bound it loosely; what matters is that we got control back at + // all, with truncation correctly flagged and protection cleanly torn down. + EXPECT_GE(depth, 0); + EXPECT_LE(depth, 1); + EXPECT_TRUE(truncated); + EXPECT_FALSE(_pt->isProtected()) << "jmp ctx must be restored after recovery"; +} + +TEST_F(StackWalkerCrashRecoveryTest, WalkDwarfRecoversFromFaultInsteadOfCrashing) { + StackContext java_ctx{}; + bool truncated = false; + const void** callchain = reinterpret_cast(_bad_page); + + int depth = StackWalker::walkDwarf(nullptr, callchain, 64, &java_ctx, &truncated); + + EXPECT_GE(depth, 0); + EXPECT_LE(depth, 1); + EXPECT_TRUE(truncated); + EXPECT_FALSE(_pt->isProtected()) << "jmp ctx must be restored after recovery"; +} + +#endif // __linux__ From 4c11e8df542ea918b3d1eeb4acd2bd7ed7cd7e04 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Thu, 30 Jul 2026 14:12:17 +0000 Subject: [PATCH 39/51] Consolidate duplicated code and fix tests --- ddprof-lib/src/main/cpp/asyncSampleMutex.h | 20 ++++++ .../src/main/cpp/hotspot/hotspotSupport.cpp | 69 ++++++++----------- ddprof-lib/src/main/cpp/jvmSupport.h | 2 - ddprof-lib/src/main/cpp/profiler.cpp | 12 ++++ ddprof-lib/src/main/cpp/profiler.h | 10 +++ ddprof-lib/src/main/cpp/threadLocalData.h | 6 +- .../test/cpp/hotspot_crash_protection_ut.cpp | 58 +++++++++------- ddprof-lib/src/test/cpp/stackWalker_ut.cpp | 67 ++++++++++++++++++ 8 files changed, 171 insertions(+), 73 deletions(-) diff --git a/ddprof-lib/src/main/cpp/asyncSampleMutex.h b/ddprof-lib/src/main/cpp/asyncSampleMutex.h index aa949ebdf7..f780cfc146 100644 --- a/ddprof-lib/src/main/cpp/asyncSampleMutex.h +++ b/ddprof-lib/src/main/cpp/asyncSampleMutex.h @@ -34,4 +34,24 @@ class AsyncSampleMutex { bool acquired() { return _acquired; } }; +// Runs `fn` while holding an AsyncSampleMutex, keeping `guard_active` true +// for exactly the mutex's lifetime (including while `fn` runs). A +// siglongjmp out of `fn` (e.g. Profiler::checkFault() recovering a SIGSEGV) +// bypasses the AsyncSampleMutex destructor, so callers landing at their +// sigsetjmp must check `guard_active` and call +// ThreadLocalData::set_unwinding_Java(false) themselves when it is still +// true on recovery -- see HotspotSupport::walkJavaStack. Pulling this out +// of walkJavaStack lets tests exercise the exact guard/flag lifetime +// pairing production code runs, instead of a hand-copied replica of it. +template +inline void withAsyncSampleGuard(ThreadLocalData *threadLocalData, + volatile bool &guard_active, Fn &&fn) { + AsyncSampleMutex mutex(threadLocalData); + if (mutex.acquired()) { + guard_active = true; + fn(); + } + guard_active = false; +} + #endif // ASYNCSAMPLEMUTEX_H diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index e4bc9d0b75..61c4bd93c4 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -1212,8 +1212,8 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { // getJavaTraceAsync() path below runs without one: it dereferences // VMThread/anchor state directly and calls into HotSpot's own // AsyncGetCallTrace. Install a jmp ctx here too, so a SIGSEGV anywhere in - // walkJavaStack is caught by Profiler::checkFault() and siglongjmp'd back - // here instead of crashing the process. + // walkJavaStack, except HotSpot's AsyncGetCallTrace call, is caught by + // Profiler::checkFault() and siglongjmp'd back here instead of crashing the process. ProfiledThread* prof_thread = ProfiledThread::current(); sigjmp_buf crash_protection_ctx; sigjmp_buf* prev_jmp_buf = prof_thread != nullptr ? prof_thread->getJmpCtx() : nullptr; @@ -1235,24 +1235,22 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { prof_thread->setJmpCtx(&crash_protection_ctx); } - if (features.mixed) { - java_frames = walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated); - } else if (isHookPrefixedSample(request.event_type)) { - if (cstack >= CSTACK_VM) { - java_frames = walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated); - } else { - AsyncSampleMutex mutex(ProfiledThread::current()); - if (mutex.acquired()) { - async_trace_active = true; - java_frames = getJavaTraceAsync(ucontext, frames, max_depth, java_ctx, truncated); - if (java_frames > 0 && java_ctx->pc != NULL && VMStructs::hasMethodStructs()) { - VMNMethod* nmethod = CodeHeap::findNMethod(java_ctx->pc); - if (nmethod != NULL) { - fillFrameTypes(frames, java_frames, nmethod); - } + // Shared by the isHookPrefixedSample and BCI_CPU/BCI_WALL async paths + // below. withAsyncSampleGuard keeps async_trace_active true for exactly + // the AsyncSampleMutex's lifetime, including the VT continuation check, + // so the sigsetjmp recovery path above sees it correctly even if a fault + // cuts the callback short. + auto walkJavaTraceAsync = [&]() { + withAsyncSampleGuard(ProfiledThread::current(), async_trace_active, [&]() { + java_frames = getJavaTraceAsync(ucontext, frames, max_depth, java_ctx, truncated); + if (java_frames > 0 && java_ctx->pc != NULL && VMStructs::hasMethodStructs()) { + VMNMethod* nmethod = CodeHeap::findNMethod(java_ctx->pc); + if (nmethod != NULL) { + fillFrameTypes(frames, java_frames, nmethod); } - async_trace_active = false; } + // ASGCT stops at the continuation boundary for virtual threads (JDK 21+). + // Append a synthetic root frame so the UI does not show "Missing Frames". if (java_frames > 0 && VM::hotspot_version() >= 21 && java_frames < max_depth) { VMThread* carrier = VMThread::current(); if (carrier != nullptr && carrier->isCarryingVirtualThread()) { @@ -1262,35 +1260,22 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { java_frames++; } } + }); + }; + + if (features.mixed) { + java_frames = walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated); + } else if (isHookPrefixedSample(request.event_type)) { + if (cstack >= CSTACK_VM) { + java_frames = walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated); + } else { + walkJavaTraceAsync(); } } else if (request.event_type == BCI_CPU || request.event_type == BCI_WALL) { if (cstack >= CSTACK_VM) { java_frames = walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated); } else { - // Async events - AsyncSampleMutex mutex(ProfiledThread::current()); - if (mutex.acquired()) { - async_trace_active = true; - java_frames = getJavaTraceAsync(ucontext, frames, max_depth, java_ctx, truncated); - if (java_frames > 0 && java_ctx->pc != NULL && VMStructs::hasMethodStructs()) { - VMNMethod* nmethod = CodeHeap::findNMethod(java_ctx->pc); - if (nmethod != NULL) { - fillFrameTypes(frames, java_frames, nmethod); - } - } - async_trace_active = false; - } - // ASGCT stops at the continuation boundary for virtual threads (JDK 21+). - // Append a synthetic root frame so the UI does not show "Missing Frames". - if (java_frames > 0 && VM::hotspot_version() >= 21 && java_frames < max_depth) { - VMThread* carrier = VMThread::current(); - if (carrier != nullptr && carrier->isCarryingVirtualThread()) { - frames[java_frames].bci = BCI_NATIVE_FRAME; - frames[java_frames].method_id = (jmethodID) "JVM Continuation"; - LP64_ONLY(frames[java_frames].padding = 0;) - java_frames++; - } - } + walkJavaTraceAsync(); } } diff --git a/ddprof-lib/src/main/cpp/jvmSupport.h b/ddprof-lib/src/main/cpp/jvmSupport.h index 8c9065adc3..8d652fed80 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.h +++ b/ddprof-lib/src/main/cpp/jvmSupport.h @@ -10,8 +10,6 @@ #include "stackFrame.h" #include "stackWalker.h" -#include - // Stack recovery techniques used to workaround AsyncGetCallTrace flaws. // Can be disabled with 'safemode' option. enum StackRecovery { diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 1196db6b17..160706bf5e 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -1045,6 +1045,18 @@ int Profiler::crashHandlerInternal(int signo, siginfo_t *siginfo, void *ucontext static std::atomic profiler_min_address{0}; static std::atomic profiler_max_address{0}; +#ifdef UNIT_TEST +void Profiler::setAddressRangeForTest(uintptr_t min, uintptr_t max) { + profiler_min_address.store(min, std::memory_order_relaxed); + profiler_max_address.store(max, std::memory_order_relaxed); +} + +void Profiler::resetAddressRangeForTest() { + profiler_min_address.store(0, std::memory_order_relaxed); + profiler_max_address.store(0, std::memory_order_relaxed); +} +#endif + void Profiler::setupSignalHandlers() { // Do not re-run the signal setup (run only when VM has not been loaded yet) if (__sync_bool_compare_and_swap(&_signals_initialized, false, true)) { diff --git a/ddprof-lib/src/main/cpp/profiler.h b/ddprof-lib/src/main/cpp/profiler.h index 5b10c133f2..4010616e9d 100644 --- a/ddprof-lib/src/main/cpp/profiler.h +++ b/ddprof-lib/src/main/cpp/profiler.h @@ -484,6 +484,16 @@ class alignas(alignof(SpinLock)) Profiler { std::pair, u64> info = _thread_info.get(tid); return info.first != nullptr ? *info.first : std::string(); } + + // Overrides the profiler address range checkFault() uses to decide + // whether a recovered fault actually originated from profiler code. + // setupSignalHandlers() never runs in gtest binaries, so the real + // profiler_min_address/profiler_max_address stay 0 there and checkFault's + // range check short-circuits via its "not initialized" fallback -- these + // let tests install a real, non-zero range so the pc < min || pc >= max + // comparison itself gets exercised, instead of being skipped entirely. + static void setAddressRangeForTest(uintptr_t min, uintptr_t max); + static void resetAddressRangeForTest(); #endif diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index 106f0fcb9e..71959c9c3f 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -59,10 +59,8 @@ class ProfiledThread : public ThreadLocalData { static ThreadLocal _current_thread; // siglongjmp buffer. Used by hotspot only at this moment. - // Published in walkVM()/recordSample() and consumed in checkFault() from an asynchronous - // SEGV-handler context on the same thread; atomic makes the publish/observe - // ordering explicit instead of relying on plain load/store, matching how - // _crash_depth is hardened below. + // Published in HotspotSupport::walkVM()/walkJavaStack() and StackWalker::walkFP()/walkDwarf() (all VMs), + // consumed in Profiler::checkFault() from an asynchronous SEGV-handler context on the same thread std::atomic _jmp_buf; u64 _pc; diff --git a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp index e025f8ee6f..25ca276e1a 100644 --- a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp +++ b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp @@ -422,22 +422,28 @@ TEST_F(SafeFetch64TocTouGuardTest, ZeroReturnMeansGiveUp) { // F. HotspotSupport::walkJavaStack()'s AsyncSampleMutex release on a // recovered fault // -// walkJavaStack() wraps its getJavaTraceAsync() branches in an -// AsyncSampleMutex, which sets ProfiledThread::is_unwinding_Java() true for -// as long as it is alive and clears it again in its destructor. A siglongjmp -// out of that region (Profiler::checkFault() recovering a fault) bypasses -// the destructor, so walkJavaStack tracks a local `async_trace_active` flag -// that mirrors the mutex's true lifetime and, on recovery, explicitly clears -// is_unwinding_Java() itself whenever that flag was set — otherwise the flag -// would stay stuck true forever and permanently disable async CPU/wall/ -// malloc/socket sampling on that thread (see hotspotSupport.cpp). +// walkJavaStack() runs its getJavaTraceAsync() branches through +// withAsyncSampleGuard() (asyncSampleMutex.h), which holds an +// AsyncSampleMutex -- setting ProfiledThread::is_unwinding_Java() true for +// as long as it is alive and clearing it again in its destructor -- and +// mirrors that lifetime into a local `async_trace_active` flag. A siglongjmp +// out of the guarded callback (Profiler::checkFault() recovering a fault) +// bypasses the AsyncSampleMutex destructor, so walkJavaStack's sigsetjmp +// recovery path explicitly clears is_unwinding_Java() itself whenever that +// flag was set — otherwise it would stay stuck true forever and permanently +// disable async CPU/wall/malloc/socket sampling on that thread (see +// hotspotSupport.cpp). // // This gtest binary has no live JVM, so getJavaTraceAsync() itself can't be // driven into a real fault — it bails out early on a null VMThread. These -// tests replicate walkJavaStack's exact protection/AsyncSampleMutex protocol -// with a real ProfiledThread, a real AsyncSampleMutex and a real -// sigsetjmp/siglongjmp fault (via the real Profiler::segvHandler -> -// Profiler::checkFault chain) to lock down the release behavior. +// tests instead call the real withAsyncSampleGuard() helper directly (the +// same one walkJavaStack calls) with a real ProfiledThread and a callback +// that faults, via a real sigsetjmp/siglongjmp fault (through the real +// Profiler::segvHandler -> Profiler::checkFault chain), to lock down the +// release behavior. Because the guard/flag lifetime pairing itself lives in +// production code rather than being hand-copied here, a regression in that +// pairing (e.g. async_trace_active being cleared before the mutex's true +// lifetime ended) is caught by these tests. // --------------------------------------------------------------------------- class WalkJavaStackAsyncMutexRecoveryTest : public ::testing::Test { @@ -470,9 +476,11 @@ class WalkJavaStackAsyncMutexRecoveryTest : public ::testing::Test { SigAction _orig_bus = nullptr; }; -// Replicates walkJavaStack's cstackgetJmpCtx(); @@ -489,16 +497,16 @@ TEST_F(WalkJavaStackAsyncMutexRecoveryTest, RecoveredFaultReleasesAsyncGuard) { } else { _pt->setJmpCtx(&crash_protection_ctx); - AsyncSampleMutex mutex(_pt); - ASSERT_TRUE(mutex.acquired()); - async_trace_active = true; - EXPECT_TRUE(_pt->is_unwinding_Java()); + withAsyncSampleGuard(_pt, async_trace_active, [&]() { + EXPECT_TRUE(_pt->is_unwinding_Java()); - // Simulate a fault inside getJavaTraceAsync's raw memory access -- - // this never returns; it lands back at the sigsetjmp above via - // Profiler::checkFault()'s siglongjmp. - *reinterpret_cast(_bad_page) = 1; - FAIL() << "unreachable: the write above must fault"; + // Simulate a fault inside the guarded callback -- this never + // returns; it lands back at the sigsetjmp above via + // Profiler::checkFault()'s siglongjmp. + *reinterpret_cast(_bad_page) = 1; + FAIL() << "unreachable: the write above must fault"; + }); + FAIL() << "unreachable: withAsyncSampleGuard must not return normally"; } EXPECT_FALSE(_pt->is_unwinding_Java()) diff --git a/ddprof-lib/src/test/cpp/stackWalker_ut.cpp b/ddprof-lib/src/test/cpp/stackWalker_ut.cpp index 5bc4536c37..b04f6cbdf8 100644 --- a/ddprof-lib/src/test/cpp/stackWalker_ut.cpp +++ b/ddprof-lib/src/test/cpp/stackWalker_ut.cpp @@ -7,9 +7,12 @@ #include "../../main/cpp/gtest_crash_handler.h" #ifdef __linux__ +#include #include +#include #include "../../main/cpp/os.h" #include "../../main/cpp/profiler.h" +#include "../../main/cpp/stackFrame.h" #include "../../main/cpp/threadLocalData.h" #endif @@ -167,10 +170,31 @@ TEST_F(StackWalkerTest, isValidSP_valid_aligned_in_range) { // the real StackWalker functions directly — no live JVM is needed since // JVMSupport::isJitCode()/JVMThread::current() both degrade to safe // not-a-JVM-thread defaults without one. +// +// checkFault() also gates recovery on the faulting pc falling inside the +// profiler library's own address range (profiler_min_address/_max_address, +// set once by Profiler::setupSignalHandlers()) — a fault whose pc is +// protected but NOT in profiler code should be left unhandled rather than +// silently swallowed. setupSignalHandlers() never runs in this gtest binary, +// so without help that range stays (0, 0) and checkFault takes its +// "not initialized" fallback, recovering unconditionally and never touching +// the pc < min || pc >= max comparison at all. SetUp() below installs a real +// range via the UNIT_TEST-only Profiler::setAddressRangeForTest() so the two +// recovery tests exercise that comparison for real (fault pc inside range), +// and CheckFaultRejectsFaultOutsideProfilerRange exercises its rejection +// side directly (fault pc outside range) — a real out-of-range SIGSEGV can't +// be used for that half since a correctly-behaving reject leaves it +// unhandled, which here means the whole test process terminates. // --------------------------------------------------------------------------- class StackWalkerCrashRecoveryTest : public ::testing::Test { protected: + // Generous enough to cover walkFP/walkDwarf's compiled code in any build + // config (debug through fully-inlined release), while remaining far + // smaller than the offset CheckFaultRejectsFaultOutsideProfilerRange + // uses to land clearly outside it. + static constexpr uintptr_t kRangeMargin = 256 * 1024; + void SetUp() override { ProfiledThread::initCurrentThread(); _pt = ProfiledThread::current(); @@ -182,9 +206,16 @@ class StackWalkerCrashRecoveryTest : public ::testing::Test { _bad_page = mmap(nullptr, 4096, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); ASSERT_NE(MAP_FAILED, _bad_page); + + uintptr_t fp_pc = reinterpret_cast(&StackWalker::walkFP); + uintptr_t dwarf_pc = reinterpret_cast(&StackWalker::walkDwarf); + _range_lo = std::min(fp_pc, dwarf_pc) - kRangeMargin; + _range_hi = std::max(fp_pc, dwarf_pc) + kRangeMargin; + Profiler::setAddressRangeForTest(_range_lo, _range_hi); } void TearDown() override { + Profiler::resetAddressRangeForTest(); munmap(_bad_page, 4096); OS::replaceSigsegvHandler(_orig_segv); OS::replaceSigbusHandler(_orig_bus); @@ -195,6 +226,8 @@ class StackWalkerCrashRecoveryTest : public ::testing::Test { void* _bad_page = nullptr; SigAction _orig_segv = nullptr; SigAction _orig_bus = nullptr; + uintptr_t _range_lo = 0; + uintptr_t _range_hi = 0; }; TEST_F(StackWalkerCrashRecoveryTest, WalkFPRecoversFromFaultInsteadOfCrashing) { @@ -230,4 +263,38 @@ TEST_F(StackWalkerCrashRecoveryTest, WalkDwarfRecoversFromFaultInsteadOfCrashing EXPECT_FALSE(_pt->isProtected()) << "jmp ctx must be restored after recovery"; } +// The two tests above only prove checkFault() recovers a fault whose pc +// falls inside the SetUp()-installed range. This drives checkFault() +// directly (bypassing segvHandler, which isn't needed to test this +// specific comparison) with a fabricated ucontext whose pc sits 256MB past +// that range -- far past kRangeMargin, so it lands outside the range +// regardless of how large walkFP/walkDwarf's compiled bodies turn out to +// be -- and confirms it returns normally rather than recovering. +TEST_F(StackWalkerCrashRecoveryTest, CheckFaultRejectsFaultOutsideProfilerRange) { + sigjmp_buf crash_protection_ctx; + bool recovered = false; + + if (sigsetjmp(crash_protection_ctx, 1) != 0) { + recovered = true; + } else { + _pt->setJmpCtx(&crash_protection_ctx); + + ucontext_t uc; + ASSERT_EQ(0, getcontext(&uc)); + StackFrame(&uc).pc() = _range_hi + (256u * 1024 * 1024); + + siginfo_t si{}; + si.si_addr = reinterpret_cast(1); + Profiler::checkFault(_pt, &si, &uc); + // Must fall through to here -- checkFault must not siglongjmp for a + // pc outside the installed range. + } + + EXPECT_FALSE(recovered) + << "checkFault must not recover a fault whose pc falls outside the profiler's own address range"; + EXPECT_TRUE(_pt->isProtected()) + << "a correctly-rejected fault must leave the jmp ctx untouched -- only the recovery path clears it"; + _pt->setJmpCtx(nullptr); +} + #endif // __linux__ From 275088bedc23b3a3dc6e17f7e544ee9517630d0f Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Thu, 30 Jul 2026 14:44:09 +0000 Subject: [PATCH 40/51] Fix musl getContext() --- ddprof-lib/src/test/cpp/stackWalker_ut.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ddprof-lib/src/test/cpp/stackWalker_ut.cpp b/ddprof-lib/src/test/cpp/stackWalker_ut.cpp index b04f6cbdf8..9c58aab831 100644 --- a/ddprof-lib/src/test/cpp/stackWalker_ut.cpp +++ b/ddprof-lib/src/test/cpp/stackWalker_ut.cpp @@ -279,8 +279,11 @@ TEST_F(StackWalkerCrashRecoveryTest, CheckFaultRejectsFaultOutsideProfilerRange) } else { _pt->setJmpCtx(&crash_protection_ctx); - ucontext_t uc; - ASSERT_EQ(0, getcontext(&uc)); + // Zero-initialized rather than populated via getcontext() -- musl + // doesn't provide getcontext(), and checkFault() only ever reads + // the pc field out of this struct, so a real, live context is + // unnecessary here. + ucontext_t uc{}; StackFrame(&uc).pc() = _range_hi + (256u * 1024 * 1024); siginfo_t si{}; From f6f16f83b3e6cdfb0ace1c59da4da010413e7fcd Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 31 Jul 2026 14:53:22 +0000 Subject: [PATCH 41/51] Fix mac/aarch64 --- .../src/main/cpp/hotspot/hotspotSupport.cpp | 5 ++++ ddprof-lib/src/main/cpp/os.h | 8 ++++++ ddprof-lib/src/main/cpp/os_linux.cpp | 4 +++ ddprof-lib/src/main/cpp/os_macos.cpp | 24 ++++++++++++++++++ ddprof-lib/src/main/cpp/threadLocalData.h | 25 +++++++++++++++++++ 5 files changed, 66 insertions(+) diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index 61c4bd93c4..1e361fc6a4 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -1222,6 +1222,11 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { // checkFault() does a siglongjmp from inside segvHandler, bypassing // segvHandler's SignalHandlerScope destructor. Compensate. SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); + // The longjmp also unwinds past getJavaTraceAsync()'s JitWriteProtection + // local without running its destructor, which would otherwise leave the + // thread's JIT write-protection register (macOS/aarch64 W^X state) stuck + // in the wrong mode. Compensate the same way. + JitWriteProtection::recoverAfterLongjmp(); prof_thread->setJmpCtx(prev_jmp_buf); if (async_trace_active) { prof_thread->set_unwinding_Java(false); diff --git a/ddprof-lib/src/main/cpp/os.h b/ddprof-lib/src/main/cpp/os.h index 6d50420ec9..3f70439953 100644 --- a/ddprof-lib/src/main/cpp/os.h +++ b/ddprof-lib/src/main/cpp/os.h @@ -92,6 +92,14 @@ class JitWriteProtection { public: JitWriteProtection(bool enable); ~JitWriteProtection(); + + // Force-restores the JIT write-protection register if a JitWriteProtection + // guard's destructor was skipped by a siglongjmp out from under it (e.g. + // HotSpot's checkFault() recovery in HotspotSupport::walkJavaStack unwinds + // past a live JitWriteProtection local without running its destructor). + // Call at the sigsetjmp landing point right after such a longjmp is known + // to have occurred. No-op if no guard is currently pending restoration. + static void recoverAfterLongjmp(); }; diff --git a/ddprof-lib/src/main/cpp/os_linux.cpp b/ddprof-lib/src/main/cpp/os_linux.cpp index ab59d8f195..8f524f8a59 100644 --- a/ddprof-lib/src/main/cpp/os_linux.cpp +++ b/ddprof-lib/src/main/cpp/os_linux.cpp @@ -107,6 +107,10 @@ JitWriteProtection::~JitWriteProtection() { // Not used on Linux } +void JitWriteProtection::recoverAfterLongjmp() { + // Not used on Linux +} + static constexpr int MAX_SIGNALS = 64; static SigAction installed_sigaction[MAX_SIGNALS]; diff --git a/ddprof-lib/src/main/cpp/os_macos.cpp b/ddprof-lib/src/main/cpp/os_macos.cpp index 0aee027040..35b8847e05 100644 --- a/ddprof-lib/src/main/cpp/os_macos.cpp +++ b/ddprof-lib/src/main/cpp/os_macos.cpp @@ -26,6 +26,7 @@ #include #include "common.h" #include "os.h" +#include "threadLocalData.h" class MacThreadList : public ThreadList { @@ -76,6 +77,12 @@ JitWriteProtection::JitWriteProtection(bool enable) { if (prev != val) { _prev = prev; _restore = true; + // No malloc/TLS lazy-init here: ProfiledThread::current() is + // pthread_getspecific-backed and AS-safe (see its declaration). + ProfiledThread* pt = ProfiledThread::current(); + if (pt != nullptr) { + pt->setJitWriteProtectionPending(prev); + } asm volatile("msr s3_6_c15_c1_5, %0\n" "isb" : "+r" (val) : : "memory"); @@ -94,6 +101,23 @@ JitWriteProtection::~JitWriteProtection() { asm volatile("msr s3_6_c15_c1_5, %0\n" "isb" : "+r" (prev) : : "memory"); + ProfiledThread* pt = ProfiledThread::current(); + if (pt != nullptr) { + pt->clearJitWriteProtectionPending(); + } + } +#endif +} + +void JitWriteProtection::recoverAfterLongjmp() { +#ifdef __aarch64__ + ProfiledThread* pt = ProfiledThread::current(); + if (pt != nullptr && pt->jitWriteProtectionPending()) { + u64 prev = pt->jitWriteProtectionSaved(); + asm volatile("msr s3_6_c15_c1_5, %0\n" + "isb" + : "+r" (prev) : : "memory"); + pt->clearJitWriteProtectionPending(); } #endif } diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index 71959c9c3f..98fdf853d7 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -79,6 +79,11 @@ class ProfiledThread : public ThreadLocalData { int _filter_slot_id; // Slot ID for thread filtering uint8_t _init_window; // Countdown for JVM thread init race window (PROF-13072) uint8_t _signal_depth; // Nested signal-handler depth (see SignalHandlerScope) +#if defined(__APPLE__) && defined(__aarch64__) + // Bookkeeping for JitWriteProtection::recoverAfterLongjmp() (os_macos.cpp). + bool _jit_write_protection_pending; + u64 _jit_write_protection_saved; +#endif UnwindFailures _unwind_failures; bool _otel_ctx_initialized; #ifdef __FAULT_INJECTION__ @@ -103,6 +108,9 @@ class ProfiledThread : public ThreadLocalData { _wall_epoch(0), _call_trace_id(0), _recording_epoch(0), _misc_flags(0), _park_block_token(0), _filter_slot_id(-1), _init_window(0), _signal_depth(0), +#if defined(__APPLE__) && defined(__aarch64__) + _jit_write_protection_pending(false), _jit_write_protection_saved(0), +#endif _otel_ctx_initialized(false), _otel_ctx_record{}, _otel_tag_encodings{}, _otel_local_root_span_id(0) { #ifdef __FAULT_INJECTION__ @@ -256,6 +264,23 @@ class ProfiledThread : public ThreadLocalData { inline void enterSignalScope() { ++_signal_depth; } inline void exitSignalScope() { if (_signal_depth > 0) --_signal_depth; } +#if defined(__APPLE__) && defined(__aarch64__) + // Tracks whether a JitWriteProtection guard on this thread has toggled the + // JIT write-protection register and is awaiting its destructor to restore + // it. Lets JitWriteProtection::recoverAfterLongjmp() (os_macos.cpp) force + // the restore from the sigsetjmp landing point in + // HotspotSupport::walkJavaStack() when a siglongjmp skipped that destructor. + // Plain member r/w is AS-safe for the same reason as signalDepth() above: + // only ever touched on the thread it belongs to. + inline bool jitWriteProtectionPending() const { return _jit_write_protection_pending; } + inline u64 jitWriteProtectionSaved() const { return _jit_write_protection_saved; } + inline void setJitWriteProtectionPending(u64 saved) { + _jit_write_protection_saved = saved; + _jit_write_protection_pending = true; + } + inline void clearJitWriteProtectionPending() { _jit_write_protection_pending = false; } +#endif + #ifdef __FAULT_INJECTION__ // One xorshift64 step (Marsaglia 2003), matching PoissonSampler::nextExp. // Plain member r/w is AS-safe: signals are delivered to the owning thread. From 505c1641230dd2142d20a02370d0c5bc1abcc675 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 31 Jul 2026 15:50:42 +0000 Subject: [PATCH 42/51] Restore ucontext --- .../src/main/cpp/hotspot/hotspotSupport.cpp | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index 1e361fc6a4..6cc5a03acd 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -1207,6 +1207,22 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { // thread. volatile bool async_trace_active = false; + // getJavaTraceAsync() (below) temporarily rewrites ucontext's pc/sp/fp to + // the Java frame via frame.restore() before calling ASGCT, and restores + // the original values before returning normally. A siglongjmp out of the + // branches below (e.g. from the post-ASGCT CodeHeap/JitCodeCache recovery + // paths) bypasses that restore, so capture the pristine register state + // here — before any rewrite can happen — and restore it explicitly on the + // recovery path. Otherwise the interrupted thread would resume execution + // at the temporary Java pc/sp instead of where it actually was. + HotspotStackFrame entry_frame(ucontext); + uintptr_t entry_pc = 0, entry_sp = 0, entry_fp = 0; + if (ucontext != NULL) { + entry_pc = entry_frame.pc(); + entry_sp = entry_frame.sp(); + entry_fp = entry_frame.fp(); + } + // walkVM() installs its own sigsetjmp/siglongjmp crash protection (chained // with any pre-existing jmp ctx, see the comment in walkVM), but the // getJavaTraceAsync() path below runs without one: it dereferences @@ -1227,6 +1243,10 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { // thread's JIT write-protection register (macOS/aarch64 W^X state) stuck // in the wrong mode. Compensate the same way. JitWriteProtection::recoverAfterLongjmp(); + // Undo any pending getJavaTraceAsync() ucontext rewrite (see comment on + // entry_pc/entry_sp/entry_fp above). No-op if ucontext is NULL or was + // never rewritten. + HotspotStackFrame(ucontext).restore(entry_pc, entry_sp, entry_fp); prof_thread->setJmpCtx(prev_jmp_buf); if (async_trace_active) { prof_thread->set_unwinding_Java(false); From e07dff9dcd704bfb600ecd3339980c2a62f52f6e Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 31 Jul 2026 19:11:45 +0200 Subject: [PATCH 43/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/test/cpp/stackWalker_ut.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ddprof-lib/src/test/cpp/stackWalker_ut.cpp b/ddprof-lib/src/test/cpp/stackWalker_ut.cpp index 9c58aab831..7ff9f28385 100644 --- a/ddprof-lib/src/test/cpp/stackWalker_ut.cpp +++ b/ddprof-lib/src/test/cpp/stackWalker_ut.cpp @@ -10,10 +10,17 @@ #include #include #include +#include "../../main/cpp/counters.h" #include "../../main/cpp/os.h" #include "../../main/cpp/profiler.h" #include "../../main/cpp/stackFrame.h" #include "../../main/cpp/threadLocalData.h" + +[[maybe_unused]] static long long* _stackwalker_ut_counters_init = Counters::getCounters(); +#endif +#include "../../main/cpp/profiler.h" +#include "../../main/cpp/stackFrame.h" +#include "../../main/cpp/threadLocalData.h" #endif static constexpr char STACKWALKER_TEST_NAME[] = "StackWalkerTest"; From 948bf807f4c3b4c814a6b658c8cef84eceb34938 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 31 Jul 2026 19:18:32 +0200 Subject: [PATCH 44/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/test/cpp/stackWalker_ut.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ddprof-lib/src/test/cpp/stackWalker_ut.cpp b/ddprof-lib/src/test/cpp/stackWalker_ut.cpp index 7ff9f28385..1212b704d2 100644 --- a/ddprof-lib/src/test/cpp/stackWalker_ut.cpp +++ b/ddprof-lib/src/test/cpp/stackWalker_ut.cpp @@ -18,10 +18,6 @@ [[maybe_unused]] static long long* _stackwalker_ut_counters_init = Counters::getCounters(); #endif -#include "../../main/cpp/profiler.h" -#include "../../main/cpp/stackFrame.h" -#include "../../main/cpp/threadLocalData.h" -#endif static constexpr char STACKWALKER_TEST_NAME[] = "StackWalkerTest"; From 7568a27bc9730dfacbe0cab68ebd6575694fae7e Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 31 Jul 2026 17:29:38 +0000 Subject: [PATCH 45/51] Fix --- ddprof-lib/src/main/cpp/asyncSampleMutex.h | 25 ++++++++++++---- .../src/main/cpp/hotspot/hotspotSupport.cpp | 13 ++++++-- ddprof-lib/src/main/cpp/os.h | 26 ++++++++++++++-- ddprof-lib/src/main/cpp/os_linux.cpp | 8 ++++- ddprof-lib/src/main/cpp/os_macos.cpp | 30 +++++++++++++++---- ddprof-lib/src/main/cpp/threadLocalData.h | 17 +++++++++-- .../test/cpp/hotspot_crash_protection_ut.cpp | 16 ++++++++++ 7 files changed, 117 insertions(+), 18 deletions(-) diff --git a/ddprof-lib/src/main/cpp/asyncSampleMutex.h b/ddprof-lib/src/main/cpp/asyncSampleMutex.h index f780cfc146..cccecbce09 100644 --- a/ddprof-lib/src/main/cpp/asyncSampleMutex.h +++ b/ddprof-lib/src/main/cpp/asyncSampleMutex.h @@ -35,7 +35,7 @@ class AsyncSampleMutex { }; // Runs `fn` while holding an AsyncSampleMutex, keeping `guard_active` true -// for exactly the mutex's lifetime (including while `fn` runs). A +// for at least the mutex's lifetime (including while `fn` runs). A // siglongjmp out of `fn` (e.g. Profiler::checkFault() recovering a SIGSEGV) // bypasses the AsyncSampleMutex destructor, so callers landing at their // sigsetjmp must check `guard_active` and call @@ -43,13 +43,28 @@ class AsyncSampleMutex { // true on recovery -- see HotspotSupport::walkJavaStack. Pulling this out // of walkJavaStack lets tests exercise the exact guard/flag lifetime // pairing production code runs, instead of a hand-copied replica of it. +// +// The mutex lives in its own nested scope so its destructor -- which clears +// is_unwinding_Java on the owning ThreadLocalData -- runs before +// `guard_active` is cleared below, not after. If `guard_active` were cleared +// first (as a single flat scope would do, since the mutex's destructor only +// runs at the function's closing brace), a second signal/fault landing in +// that gap and recovered via the caller's sigsetjmp would see `guard_active +// == false`, skip the compensating clear, and never get another chance -- +// the real siglongjmp already bypassed the destructor -- leaving +// is_unwinding_Java stuck true and the thread permanently excluded from +// async sampling. Clearing `guard_active` only after the destructor has +// already run makes a fault in that gap a harmless redundant compensation +// instead of a missed one. template inline void withAsyncSampleGuard(ThreadLocalData *threadLocalData, volatile bool &guard_active, Fn &&fn) { - AsyncSampleMutex mutex(threadLocalData); - if (mutex.acquired()) { - guard_active = true; - fn(); + { + AsyncSampleMutex mutex(threadLocalData); + if (mutex.acquired()) { + guard_active = true; + fn(); + } } guard_active = false; } diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index d302145283..cc03b4f547 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -1233,11 +1233,20 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { // getJavaTraceAsync() path below runs without one: it dereferences // VMThread/anchor state directly and calls into HotSpot's own // AsyncGetCallTrace. Install a jmp ctx here too, so a SIGSEGV anywhere in - // walkJavaStack, except HotSpot's AsyncGetCallTrace call, is caught by + // walkJavaStack, except HotSpot's AsyncGetCallTrace call, is caught by // Profiler::checkFault() and siglongjmp'd back here instead of crashing the process. ProfiledThread* prof_thread = ProfiledThread::current(); sigjmp_buf crash_protection_ctx; sigjmp_buf* prev_jmp_buf = prof_thread != nullptr ? prof_thread->getJmpCtx() : nullptr; + // Watermark for JitWriteProtection::recoverAfterLongjmp() below: only a + // guard constructed after this point (i.e. getJavaTraceAsync()'s own, from + // this call) may be force-restored on recovery. Without it, a guard + // already pending here -- e.g. this thread was interrupted by a profiling + // signal while already inside Profiler::updateThreadName's or VM::ready's + // JitWriteProtection -- would be wrongly force-restored while that outer + // guard is still live, flipping the register out from under code that + // will resume and expects its own guard's mode. + u32 jit_write_protection_watermark = JitWriteProtection::currentGeneration(); if (prof_thread != nullptr && sigsetjmp(crash_protection_ctx, 1) != 0) { // checkFault() does a siglongjmp from inside segvHandler, bypassing @@ -1247,7 +1256,7 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { // local without running its destructor, which would otherwise leave the // thread's JIT write-protection register (macOS/aarch64 W^X state) stuck // in the wrong mode. Compensate the same way. - JitWriteProtection::recoverAfterLongjmp(); + JitWriteProtection::recoverAfterLongjmp(jit_write_protection_watermark); // Undo any pending getJavaTraceAsync() ucontext rewrite (see comment on // entry_pc/entry_sp/entry_fp above). No-op if ucontext is NULL or was // never rewritten. diff --git a/ddprof-lib/src/main/cpp/os.h b/ddprof-lib/src/main/cpp/os.h index 3f70439953..4555881845 100644 --- a/ddprof-lib/src/main/cpp/os.h +++ b/ddprof-lib/src/main/cpp/os.h @@ -88,18 +88,40 @@ class JitWriteProtection { private: u64 _prev; bool _restore; + // Generation stamp assigned by ProfiledThread::setJitWriteProtectionPending() + // when this instance toggles the register (_restore == true). Lets + // recoverAfterLongjmp() tell "a guard created after my watermark" apart + // from an outer, still-alive guard elsewhere on the stack; see its comment. + u32 _generation; public: JitWriteProtection(bool enable); ~JitWriteProtection(); + // Snapshot of the thread's current JitWriteProtection generation counter, + // to be captured before installing a sigsetjmp and passed to + // recoverAfterLongjmp() on that sigsetjmp's landing path. Returns 0 (never + // a valid generation) if there is no ProfiledThread for the current thread. + static u32 currentGeneration(); + // Force-restores the JIT write-protection register if a JitWriteProtection // guard's destructor was skipped by a siglongjmp out from under it (e.g. // HotSpot's checkFault() recovery in HotspotSupport::walkJavaStack unwinds // past a live JitWriteProtection local without running its destructor). // Call at the sigsetjmp landing point right after such a longjmp is known - // to have occurred. No-op if no guard is currently pending restoration. - static void recoverAfterLongjmp(); + // to have occurred, passing the generation captured via currentGeneration() + // right before that sigsetjmp was installed. + // + // Only compensates a guard whose generation is newer than `watermark` -- + // i.e. one constructed strictly inside the region that sigsetjmp + // protects. A guard already pending at `watermark` belongs to an outer, + // still-alive stack frame (e.g. this thread was merely interrupted by a + // profiling signal while already inside Profiler::updateThreadName's or + // VM::ready's JitWriteProtection guard) and must be left alone: it is + // not the guard this particular longjmp bypassed, and force-restoring it + // here would flip the register out from under code that is still live + // and will resume expecting its guard's mode. + static void recoverAfterLongjmp(u32 watermark); }; diff --git a/ddprof-lib/src/main/cpp/os_linux.cpp b/ddprof-lib/src/main/cpp/os_linux.cpp index 8f524f8a59..0d69508c8c 100644 --- a/ddprof-lib/src/main/cpp/os_linux.cpp +++ b/ddprof-lib/src/main/cpp/os_linux.cpp @@ -107,8 +107,14 @@ JitWriteProtection::~JitWriteProtection() { // Not used on Linux } -void JitWriteProtection::recoverAfterLongjmp() { +u32 JitWriteProtection::currentGeneration() { // Not used on Linux + return 0; +} + +void JitWriteProtection::recoverAfterLongjmp(u32 watermark) { + // Not used on Linux + (void)watermark; } diff --git a/ddprof-lib/src/main/cpp/os_macos.cpp b/ddprof-lib/src/main/cpp/os_macos.cpp index 35b8847e05..58c5ffe5a1 100644 --- a/ddprof-lib/src/main/cpp/os_macos.cpp +++ b/ddprof-lib/src/main/cpp/os_macos.cpp @@ -80,9 +80,7 @@ JitWriteProtection::JitWriteProtection(bool enable) { // No malloc/TLS lazy-init here: ProfiledThread::current() is // pthread_getspecific-backed and AS-safe (see its declaration). ProfiledThread* pt = ProfiledThread::current(); - if (pt != nullptr) { - pt->setJitWriteProtectionPending(prev); - } + _generation = (pt != nullptr) ? pt->setJitWriteProtectionPending(prev) : 0; asm volatile("msr s3_6_c15_c1_5, %0\n" "isb" : "+r" (val) : : "memory"); @@ -102,17 +100,37 @@ JitWriteProtection::~JitWriteProtection() { "isb" : "+r" (prev) : : "memory"); ProfiledThread* pt = ProfiledThread::current(); - if (pt != nullptr) { + // Only clear the thread's pending marker if it still refers to this + // guard. A nested guard that constructed and destructed entirely + // within our lifetime already bumped the generation and left the + // marker in whatever state is correct for it; clearing unconditionally + // here would wrongly erase that newer guard's bookkeeping. + if (pt != nullptr && pt->jitWriteProtectionGeneration() == _generation) { pt->clearJitWriteProtectionPending(); } } #endif } -void JitWriteProtection::recoverAfterLongjmp() { +u32 JitWriteProtection::currentGeneration() { +#ifdef __aarch64__ + ProfiledThread* pt = ProfiledThread::current(); + return pt != nullptr ? pt->jitWriteProtectionGeneration() : 0; +#else + return 0; +#endif +} + +void JitWriteProtection::recoverAfterLongjmp(u32 watermark) { #ifdef __aarch64__ ProfiledThread* pt = ProfiledThread::current(); - if (pt != nullptr && pt->jitWriteProtectionPending()) { + // Only compensate a guard constructed after `watermark` was captured -- + // i.e. strictly inside the sigsetjmp-protected region that is now + // recovering. A guard already pending at `watermark` belongs to an + // outer, still-alive stack frame and must be left alone; see the + // extended rationale on the declaration in os.h. + if (pt != nullptr && pt->jitWriteProtectionPending() && + pt->jitWriteProtectionGeneration() != watermark) { u64 prev = pt->jitWriteProtectionSaved(); asm volatile("msr s3_6_c15_c1_5, %0\n" "isb" diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index 98fdf853d7..ef9ceb023c 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -82,6 +82,7 @@ class ProfiledThread : public ThreadLocalData { #if defined(__APPLE__) && defined(__aarch64__) // Bookkeeping for JitWriteProtection::recoverAfterLongjmp() (os_macos.cpp). bool _jit_write_protection_pending; + u32 _jit_write_protection_generation; u64 _jit_write_protection_saved; #endif UnwindFailures _unwind_failures; @@ -109,7 +110,8 @@ class ProfiledThread : public ThreadLocalData { _park_block_token(0), _filter_slot_id(-1), _init_window(0), _signal_depth(0), #if defined(__APPLE__) && defined(__aarch64__) - _jit_write_protection_pending(false), _jit_write_protection_saved(0), + _jit_write_protection_pending(false), _jit_write_protection_generation(0), + _jit_write_protection_saved(0), #endif _otel_ctx_initialized(false), _otel_ctx_record{}, _otel_tag_encodings{}, _otel_local_root_span_id(0) { @@ -272,11 +274,22 @@ class ProfiledThread : public ThreadLocalData { // HotspotSupport::walkJavaStack() when a siglongjmp skipped that destructor. // Plain member r/w is AS-safe for the same reason as signalDepth() above: // only ever touched on the thread it belongs to. + // + // _jit_write_protection_generation is a monotonic per-thread counter bumped + // every time a guard newly becomes pending. It lets a sigsetjmp site tell + // "the guard pending right now is one I watermarked before, i.e. it + // belongs to an outer, still-alive frame" apart from "a guard created + // after my watermark, i.e. nested strictly inside the region I protect" -- + // see JitWriteProtection::recoverAfterLongjmp()'s comment in os.h. inline bool jitWriteProtectionPending() const { return _jit_write_protection_pending; } inline u64 jitWriteProtectionSaved() const { return _jit_write_protection_saved; } - inline void setJitWriteProtectionPending(u64 saved) { + inline u32 jitWriteProtectionGeneration() const { return _jit_write_protection_generation; } + // Records a newly-toggled guard and returns the generation stamp assigned + // to it, for the guard to remember and compare against on its own destructor. + inline u32 setJitWriteProtectionPending(u64 saved) { _jit_write_protection_saved = saved; _jit_write_protection_pending = true; + return ++_jit_write_protection_generation; } inline void clearJitWriteProtectionPending() { _jit_write_protection_pending = false; } #endif diff --git a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp index 25ca276e1a..b8c472ac9d 100644 --- a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp +++ b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp @@ -34,6 +34,7 @@ #include "profiler.h" #include "asyncSampleMutex.h" +#include "counters.h" #include "jvmThread.h" #include "safeAccess.h" #include "os.h" @@ -44,6 +45,21 @@ #include #include +namespace { +// Force Counters::instance()'s function-local static to construct here, off +// the signal path -- mirrors Profiler::setupSignalHandlers()'s own eager +// warm-up (profiler.cpp), which exists precisely because the first touch of +// the singleton runs non-async-signal-safe static-initialization machinery +// (a C++ guard-variable lock, then aligned_alloc/memset). Several fixtures +// below install real signal handlers (Profiler::segvHandler -> checkFault(), +// SafeAccess::handle_safefetch) that increment Counters from inside an +// actual SIGSEGV, without going through setupSignalHandlers() first. Without +// this, whichever such test happens to run first in the process would race +// that first-touch initialization inside a real signal handler instead of +// ordinary code -- not async-signal-safe, and liable to hang. +const bool kCountersPrewarmed = (Counters::getCounters(), true); +} // namespace + // --------------------------------------------------------------------------- // A. ProfiledThread thread-type classification (isJavaThread fast path) // From 6c6a3d7735f00637a8336edb0692d5658318f69f Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 31 Jul 2026 18:41:20 +0000 Subject: [PATCH 46/51] Fix test --- .../test/cpp/hotspot_crash_protection_ut.cpp | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp index b8c472ac9d..47603c03c7 100644 --- a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp +++ b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp @@ -45,21 +45,6 @@ #include #include -namespace { -// Force Counters::instance()'s function-local static to construct here, off -// the signal path -- mirrors Profiler::setupSignalHandlers()'s own eager -// warm-up (profiler.cpp), which exists precisely because the first touch of -// the singleton runs non-async-signal-safe static-initialization machinery -// (a C++ guard-variable lock, then aligned_alloc/memset). Several fixtures -// below install real signal handlers (Profiler::segvHandler -> checkFault(), -// SafeAccess::handle_safefetch) that increment Counters from inside an -// actual SIGSEGV, without going through setupSignalHandlers() first. Without -// this, whichever such test happens to run first in the process would race -// that first-touch initialization inside a real signal handler instead of -// ordinary code -- not async-signal-safe, and liable to hang. -const bool kCountersPrewarmed = (Counters::getCounters(), true); -} // namespace - // --------------------------------------------------------------------------- // A. ProfiledThread thread-type classification (isJavaThread fast path) // @@ -400,6 +385,17 @@ class SafeFetch64TocTouGuardTest : public ::testing::Test { } void SetUp() override { + // Force Counters::instance()'s function-local static to construct + // here, off the signal path, before the handler below can fire -- + // mirrors Profiler::setupSignalHandlers()'s own eager warm-up + // (profiler.cpp), which exists precisely because the first touch of + // the singleton runs non-async-signal-safe static-initialization + // machinery (a C++ guard-variable lock, then aligned_alloc/memset; + // see AGENTS.md's signal-handler-safety rule). handle_safefetch() + // below increments SAFEFETCH_FAILED/SAFECOPY_FAILED from inside a + // real SIGSEGV, without going through setupSignalHandlers() first. + (void)Counters::getCounters(); + _orig_segv = OS::replaceSigsegvHandler(handler); _orig_bus = OS::replaceSigbusHandler(handler); } @@ -471,6 +467,18 @@ class WalkJavaStackAsyncMutexRecoveryTest : public ::testing::Test { ASSERT_FALSE(_pt->isProtected()); ASSERT_FALSE(_pt->is_unwinding_Java()); + // Force Counters::instance()'s function-local static to construct + // here, off the signal path, before Profiler::segvHandler below can + // fire -- mirrors Profiler::setupSignalHandlers()'s own eager + // warm-up (profiler.cpp), which exists precisely because the first + // touch of the singleton runs non-async-signal-safe + // static-initialization machinery (a C++ guard-variable lock, then + // aligned_alloc/memset; see AGENTS.md's signal-handler-safety rule). + // The recovered fault below reaches Profiler::checkFault(), which + // increments STACKWALK_LONGJMP_RECOVERED, from inside a real SIGSEGV, + // without going through setupSignalHandlers() first. + (void)Counters::getCounters(); + _orig_segv = OS::replaceSigsegvHandler(Profiler::segvHandler); _orig_bus = OS::replaceSigbusHandler(Profiler::busHandler); From 42432ff581f2669cfd9160abeed061fa6418b48f Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 31 Jul 2026 18:55:01 +0000 Subject: [PATCH 47/51] Add copyright --- ddprof-lib/src/main/cpp/asyncSampleMutex.h | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ddprof-lib/src/main/cpp/asyncSampleMutex.h b/ddprof-lib/src/main/cpp/asyncSampleMutex.h index cccecbce09..351639c37f 100644 --- a/ddprof-lib/src/main/cpp/asyncSampleMutex.h +++ b/ddprof-lib/src/main/cpp/asyncSampleMutex.h @@ -1,3 +1,19 @@ +/* + * Copyright 2026, Datadog, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #ifndef ASYNCSAMPLEMUTEX_H #define ASYNCSAMPLEMUTEX_H From be7e84dcaf4fabfeee3a85d609b1a9131dffee92 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 31 Jul 2026 20:46:11 +0000 Subject: [PATCH 48/51] Revert code consolidation change in HotspotSupport::getJavaTraceAsync() --- ddprof-lib/src/main/cpp/asyncSampleMutex.h | 35 ----- .../src/main/cpp/hotspot/hotspotSupport.cpp | 92 ++++------- ddprof-lib/src/main/cpp/os.h | 30 ---- ddprof-lib/src/main/cpp/os_linux.cpp | 10 -- ddprof-lib/src/main/cpp/os_macos.cpp | 42 ----- ddprof-lib/src/main/cpp/threadLocalData.h | 38 ----- .../test/cpp/hotspot_crash_protection_ut.cpp | 147 ------------------ 7 files changed, 33 insertions(+), 361 deletions(-) diff --git a/ddprof-lib/src/main/cpp/asyncSampleMutex.h b/ddprof-lib/src/main/cpp/asyncSampleMutex.h index 351639c37f..88af376a05 100644 --- a/ddprof-lib/src/main/cpp/asyncSampleMutex.h +++ b/ddprof-lib/src/main/cpp/asyncSampleMutex.h @@ -50,39 +50,4 @@ class AsyncSampleMutex { bool acquired() { return _acquired; } }; -// Runs `fn` while holding an AsyncSampleMutex, keeping `guard_active` true -// for at least the mutex's lifetime (including while `fn` runs). A -// siglongjmp out of `fn` (e.g. Profiler::checkFault() recovering a SIGSEGV) -// bypasses the AsyncSampleMutex destructor, so callers landing at their -// sigsetjmp must check `guard_active` and call -// ThreadLocalData::set_unwinding_Java(false) themselves when it is still -// true on recovery -- see HotspotSupport::walkJavaStack. Pulling this out -// of walkJavaStack lets tests exercise the exact guard/flag lifetime -// pairing production code runs, instead of a hand-copied replica of it. -// -// The mutex lives in its own nested scope so its destructor -- which clears -// is_unwinding_Java on the owning ThreadLocalData -- runs before -// `guard_active` is cleared below, not after. If `guard_active` were cleared -// first (as a single flat scope would do, since the mutex's destructor only -// runs at the function's closing brace), a second signal/fault landing in -// that gap and recovered via the caller's sigsetjmp would see `guard_active -// == false`, skip the compensating clear, and never get another chance -- -// the real siglongjmp already bypassed the destructor -- leaving -// is_unwinding_Java stuck true and the thread permanently excluded from -// async sampling. Clearing `guard_active` only after the destructor has -// already run makes a fault in that gap a harmless redundant compensation -// instead of a missed one. -template -inline void withAsyncSampleGuard(ThreadLocalData *threadLocalData, - volatile bool &guard_active, Fn &&fn) { - { - AsyncSampleMutex mutex(threadLocalData); - if (mutex.acquired()) { - guard_active = true; - fn(); - } - } - guard_active = false; -} - #endif // ASYNCSAMPLEMUTEX_H diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index cc03b4f547..c87273d4a9 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -1212,22 +1212,6 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { // thread. volatile bool async_trace_active = false; - // getJavaTraceAsync() (below) temporarily rewrites ucontext's pc/sp/fp to - // the Java frame via frame.restore() before calling ASGCT, and restores - // the original values before returning normally. A siglongjmp out of the - // branches below (e.g. from the post-ASGCT CodeHeap/JitCodeCache recovery - // paths) bypasses that restore, so capture the pristine register state - // here — before any rewrite can happen — and restore it explicitly on the - // recovery path. Otherwise the interrupted thread would resume execution - // at the temporary Java pc/sp instead of where it actually was. - HotspotStackFrame entry_frame(ucontext); - uintptr_t entry_pc = 0, entry_sp = 0, entry_fp = 0; - if (ucontext != NULL) { - entry_pc = entry_frame.pc(); - entry_sp = entry_frame.sp(); - entry_fp = entry_frame.fp(); - } - // walkVM() installs its own sigsetjmp/siglongjmp crash protection (chained // with any pre-existing jmp ctx, see the comment in walkVM), but the // getJavaTraceAsync() path below runs without one: it dereferences @@ -1238,29 +1222,11 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { ProfiledThread* prof_thread = ProfiledThread::current(); sigjmp_buf crash_protection_ctx; sigjmp_buf* prev_jmp_buf = prof_thread != nullptr ? prof_thread->getJmpCtx() : nullptr; - // Watermark for JitWriteProtection::recoverAfterLongjmp() below: only a - // guard constructed after this point (i.e. getJavaTraceAsync()'s own, from - // this call) may be force-restored on recovery. Without it, a guard - // already pending here -- e.g. this thread was interrupted by a profiling - // signal while already inside Profiler::updateThreadName's or VM::ready's - // JitWriteProtection -- would be wrongly force-restored while that outer - // guard is still live, flipping the register out from under code that - // will resume and expects its own guard's mode. - u32 jit_write_protection_watermark = JitWriteProtection::currentGeneration(); if (prof_thread != nullptr && sigsetjmp(crash_protection_ctx, 1) != 0) { // checkFault() does a siglongjmp from inside segvHandler, bypassing // segvHandler's SignalHandlerScope destructor. Compensate. SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); - // The longjmp also unwinds past getJavaTraceAsync()'s JitWriteProtection - // local without running its destructor, which would otherwise leave the - // thread's JIT write-protection register (macOS/aarch64 W^X state) stuck - // in the wrong mode. Compensate the same way. - JitWriteProtection::recoverAfterLongjmp(jit_write_protection_watermark); - // Undo any pending getJavaTraceAsync() ucontext rewrite (see comment on - // entry_pc/entry_sp/entry_fp above). No-op if ucontext is NULL or was - // never rewritten. - HotspotStackFrame(ucontext).restore(entry_pc, entry_sp, entry_fp); prof_thread->setJmpCtx(prev_jmp_buf); if (async_trace_active) { prof_thread->set_unwinding_Java(false); @@ -1274,22 +1240,22 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { prof_thread->setJmpCtx(&crash_protection_ctx); } - // Shared by the isHookPrefixedSample and BCI_CPU/BCI_WALL async paths - // below. withAsyncSampleGuard keeps async_trace_active true for exactly - // the AsyncSampleMutex's lifetime, including the VT continuation check, - // so the sigsetjmp recovery path above sees it correctly even if a fault - // cuts the callback short. - auto walkJavaTraceAsync = [&]() { - withAsyncSampleGuard(ProfiledThread::current(), async_trace_active, [&]() { - java_frames = getJavaTraceAsync(ucontext, frames, max_depth, java_ctx, truncated); - if (java_frames > 0 && java_ctx->pc != NULL && VMStructs::hasMethodStructs()) { - VMNMethod* nmethod = CodeHeap::findNMethod(java_ctx->pc); - if (nmethod != NULL) { - fillFrameTypes(frames, java_frames, nmethod); + if (features.mixed) { + java_frames = walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated); + } else if (isHookPrefixedSample(request.event_type)) { + if (cstack >= CSTACK_VM) { + java_frames = walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated); + } else { + AsyncSampleMutex mutex(ProfiledThread::current()); + if (mutex.acquired()) { + java_frames = getJavaTraceAsync(ucontext, frames, max_depth, java_ctx, truncated); + if (java_frames > 0 && java_ctx->pc != NULL && VMStructs::hasMethodStructs()) { + VMNMethod* nmethod = CodeHeap::findNMethod(java_ctx->pc); + if (nmethod != NULL) { + fillFrameTypes(frames, java_frames, nmethod); + } } } - // ASGCT stops at the continuation boundary for virtual threads (JDK 21+). - // Append a synthetic root frame so the UI does not show "Missing Frames". if (java_frames > 0 && VM::hotspot_version() >= 21 && java_frames < max_depth) { VMThread* carrier = VMThread::current(); if (carrier != nullptr && carrier->isCarryingVirtualThread()) { @@ -1299,22 +1265,30 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { java_frames++; } } - }); - }; - - if (features.mixed) { - java_frames = walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated); - } else if (isHookPrefixedSample(request.event_type)) { - if (cstack >= CSTACK_VM) { - java_frames = walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated); - } else { - walkJavaTraceAsync(); } } else if (request.event_type == BCI_CPU || request.event_type == BCI_WALL) { if (cstack >= CSTACK_VM) { java_frames = walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated); } else { - walkJavaTraceAsync(); + AsyncSampleMutex mutex(ProfiledThread::current()); + if (mutex.acquired()) { + java_frames = getJavaTraceAsync(ucontext, frames, max_depth, java_ctx, truncated); + if (java_frames > 0 && java_ctx->pc != NULL && VMStructs::hasMethodStructs()) { + VMNMethod* nmethod = CodeHeap::findNMethod(java_ctx->pc); + if (nmethod != NULL) { + fillFrameTypes(frames, java_frames, nmethod); + } + } + } + if (java_frames > 0 && VM::hotspot_version() >= 21 && java_frames < max_depth) { + VMThread* carrier = VMThread::current(); + if (carrier != nullptr && carrier->isCarryingVirtualThread()) { + frames[java_frames].bci = BCI_NATIVE_FRAME; + frames[java_frames].method_id = (jmethodID) "JVM Continuation"; + LP64_ONLY(frames[java_frames].padding = 0;) + java_frames++; + } + } } } diff --git a/ddprof-lib/src/main/cpp/os.h b/ddprof-lib/src/main/cpp/os.h index 4555881845..6d50420ec9 100644 --- a/ddprof-lib/src/main/cpp/os.h +++ b/ddprof-lib/src/main/cpp/os.h @@ -88,40 +88,10 @@ class JitWriteProtection { private: u64 _prev; bool _restore; - // Generation stamp assigned by ProfiledThread::setJitWriteProtectionPending() - // when this instance toggles the register (_restore == true). Lets - // recoverAfterLongjmp() tell "a guard created after my watermark" apart - // from an outer, still-alive guard elsewhere on the stack; see its comment. - u32 _generation; public: JitWriteProtection(bool enable); ~JitWriteProtection(); - - // Snapshot of the thread's current JitWriteProtection generation counter, - // to be captured before installing a sigsetjmp and passed to - // recoverAfterLongjmp() on that sigsetjmp's landing path. Returns 0 (never - // a valid generation) if there is no ProfiledThread for the current thread. - static u32 currentGeneration(); - - // Force-restores the JIT write-protection register if a JitWriteProtection - // guard's destructor was skipped by a siglongjmp out from under it (e.g. - // HotSpot's checkFault() recovery in HotspotSupport::walkJavaStack unwinds - // past a live JitWriteProtection local without running its destructor). - // Call at the sigsetjmp landing point right after such a longjmp is known - // to have occurred, passing the generation captured via currentGeneration() - // right before that sigsetjmp was installed. - // - // Only compensates a guard whose generation is newer than `watermark` -- - // i.e. one constructed strictly inside the region that sigsetjmp - // protects. A guard already pending at `watermark` belongs to an outer, - // still-alive stack frame (e.g. this thread was merely interrupted by a - // profiling signal while already inside Profiler::updateThreadName's or - // VM::ready's JitWriteProtection guard) and must be left alone: it is - // not the guard this particular longjmp bypassed, and force-restoring it - // here would flip the register out from under code that is still live - // and will resume expecting its guard's mode. - static void recoverAfterLongjmp(u32 watermark); }; diff --git a/ddprof-lib/src/main/cpp/os_linux.cpp b/ddprof-lib/src/main/cpp/os_linux.cpp index 0d69508c8c..ab59d8f195 100644 --- a/ddprof-lib/src/main/cpp/os_linux.cpp +++ b/ddprof-lib/src/main/cpp/os_linux.cpp @@ -107,16 +107,6 @@ JitWriteProtection::~JitWriteProtection() { // Not used on Linux } -u32 JitWriteProtection::currentGeneration() { - // Not used on Linux - return 0; -} - -void JitWriteProtection::recoverAfterLongjmp(u32 watermark) { - // Not used on Linux - (void)watermark; -} - static constexpr int MAX_SIGNALS = 64; static SigAction installed_sigaction[MAX_SIGNALS]; diff --git a/ddprof-lib/src/main/cpp/os_macos.cpp b/ddprof-lib/src/main/cpp/os_macos.cpp index 58c5ffe5a1..0aee027040 100644 --- a/ddprof-lib/src/main/cpp/os_macos.cpp +++ b/ddprof-lib/src/main/cpp/os_macos.cpp @@ -26,7 +26,6 @@ #include #include "common.h" #include "os.h" -#include "threadLocalData.h" class MacThreadList : public ThreadList { @@ -77,10 +76,6 @@ JitWriteProtection::JitWriteProtection(bool enable) { if (prev != val) { _prev = prev; _restore = true; - // No malloc/TLS lazy-init here: ProfiledThread::current() is - // pthread_getspecific-backed and AS-safe (see its declaration). - ProfiledThread* pt = ProfiledThread::current(); - _generation = (pt != nullptr) ? pt->setJitWriteProtectionPending(prev) : 0; asm volatile("msr s3_6_c15_c1_5, %0\n" "isb" : "+r" (val) : : "memory"); @@ -99,43 +94,6 @@ JitWriteProtection::~JitWriteProtection() { asm volatile("msr s3_6_c15_c1_5, %0\n" "isb" : "+r" (prev) : : "memory"); - ProfiledThread* pt = ProfiledThread::current(); - // Only clear the thread's pending marker if it still refers to this - // guard. A nested guard that constructed and destructed entirely - // within our lifetime already bumped the generation and left the - // marker in whatever state is correct for it; clearing unconditionally - // here would wrongly erase that newer guard's bookkeeping. - if (pt != nullptr && pt->jitWriteProtectionGeneration() == _generation) { - pt->clearJitWriteProtectionPending(); - } - } -#endif -} - -u32 JitWriteProtection::currentGeneration() { -#ifdef __aarch64__ - ProfiledThread* pt = ProfiledThread::current(); - return pt != nullptr ? pt->jitWriteProtectionGeneration() : 0; -#else - return 0; -#endif -} - -void JitWriteProtection::recoverAfterLongjmp(u32 watermark) { -#ifdef __aarch64__ - ProfiledThread* pt = ProfiledThread::current(); - // Only compensate a guard constructed after `watermark` was captured -- - // i.e. strictly inside the sigsetjmp-protected region that is now - // recovering. A guard already pending at `watermark` belongs to an - // outer, still-alive stack frame and must be left alone; see the - // extended rationale on the declaration in os.h. - if (pt != nullptr && pt->jitWriteProtectionPending() && - pt->jitWriteProtectionGeneration() != watermark) { - u64 prev = pt->jitWriteProtectionSaved(); - asm volatile("msr s3_6_c15_c1_5, %0\n" - "isb" - : "+r" (prev) : : "memory"); - pt->clearJitWriteProtectionPending(); } #endif } diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index ef9ceb023c..71959c9c3f 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -79,12 +79,6 @@ class ProfiledThread : public ThreadLocalData { int _filter_slot_id; // Slot ID for thread filtering uint8_t _init_window; // Countdown for JVM thread init race window (PROF-13072) uint8_t _signal_depth; // Nested signal-handler depth (see SignalHandlerScope) -#if defined(__APPLE__) && defined(__aarch64__) - // Bookkeeping for JitWriteProtection::recoverAfterLongjmp() (os_macos.cpp). - bool _jit_write_protection_pending; - u32 _jit_write_protection_generation; - u64 _jit_write_protection_saved; -#endif UnwindFailures _unwind_failures; bool _otel_ctx_initialized; #ifdef __FAULT_INJECTION__ @@ -109,10 +103,6 @@ class ProfiledThread : public ThreadLocalData { _wall_epoch(0), _call_trace_id(0), _recording_epoch(0), _misc_flags(0), _park_block_token(0), _filter_slot_id(-1), _init_window(0), _signal_depth(0), -#if defined(__APPLE__) && defined(__aarch64__) - _jit_write_protection_pending(false), _jit_write_protection_generation(0), - _jit_write_protection_saved(0), -#endif _otel_ctx_initialized(false), _otel_ctx_record{}, _otel_tag_encodings{}, _otel_local_root_span_id(0) { #ifdef __FAULT_INJECTION__ @@ -266,34 +256,6 @@ class ProfiledThread : public ThreadLocalData { inline void enterSignalScope() { ++_signal_depth; } inline void exitSignalScope() { if (_signal_depth > 0) --_signal_depth; } -#if defined(__APPLE__) && defined(__aarch64__) - // Tracks whether a JitWriteProtection guard on this thread has toggled the - // JIT write-protection register and is awaiting its destructor to restore - // it. Lets JitWriteProtection::recoverAfterLongjmp() (os_macos.cpp) force - // the restore from the sigsetjmp landing point in - // HotspotSupport::walkJavaStack() when a siglongjmp skipped that destructor. - // Plain member r/w is AS-safe for the same reason as signalDepth() above: - // only ever touched on the thread it belongs to. - // - // _jit_write_protection_generation is a monotonic per-thread counter bumped - // every time a guard newly becomes pending. It lets a sigsetjmp site tell - // "the guard pending right now is one I watermarked before, i.e. it - // belongs to an outer, still-alive frame" apart from "a guard created - // after my watermark, i.e. nested strictly inside the region I protect" -- - // see JitWriteProtection::recoverAfterLongjmp()'s comment in os.h. - inline bool jitWriteProtectionPending() const { return _jit_write_protection_pending; } - inline u64 jitWriteProtectionSaved() const { return _jit_write_protection_saved; } - inline u32 jitWriteProtectionGeneration() const { return _jit_write_protection_generation; } - // Records a newly-toggled guard and returns the generation stamp assigned - // to it, for the guard to remember and compare against on its own destructor. - inline u32 setJitWriteProtectionPending(u64 saved) { - _jit_write_protection_saved = saved; - _jit_write_protection_pending = true; - return ++_jit_write_protection_generation; - } - inline void clearJitWriteProtectionPending() { _jit_write_protection_pending = false; } -#endif - #ifdef __FAULT_INJECTION__ // One xorshift64 step (Marsaglia 2003), matching PoissonSampler::nextExp. // Plain member r/w is AS-safe: signals are delivered to the owning thread. diff --git a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp index 47603c03c7..3739606bbb 100644 --- a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp +++ b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp @@ -34,7 +34,6 @@ #include "profiler.h" #include "asyncSampleMutex.h" -#include "counters.h" #include "jvmThread.h" #include "safeAccess.h" #include "os.h" @@ -385,17 +384,6 @@ class SafeFetch64TocTouGuardTest : public ::testing::Test { } void SetUp() override { - // Force Counters::instance()'s function-local static to construct - // here, off the signal path, before the handler below can fire -- - // mirrors Profiler::setupSignalHandlers()'s own eager warm-up - // (profiler.cpp), which exists precisely because the first touch of - // the singleton runs non-async-signal-safe static-initialization - // machinery (a C++ guard-variable lock, then aligned_alloc/memset; - // see AGENTS.md's signal-handler-safety rule). handle_safefetch() - // below increments SAFEFETCH_FAILED/SAFECOPY_FAILED from inside a - // real SIGSEGV, without going through setupSignalHandlers() first. - (void)Counters::getCounters(); - _orig_segv = OS::replaceSigsegvHandler(handler); _orig_bus = OS::replaceSigbusHandler(handler); } @@ -430,139 +418,4 @@ TEST_F(SafeFetch64TocTouGuardTest, ZeroReturnMeansGiveUp) { munmap(page, 4096); } -// --------------------------------------------------------------------------- -// F. HotspotSupport::walkJavaStack()'s AsyncSampleMutex release on a -// recovered fault -// -// walkJavaStack() runs its getJavaTraceAsync() branches through -// withAsyncSampleGuard() (asyncSampleMutex.h), which holds an -// AsyncSampleMutex -- setting ProfiledThread::is_unwinding_Java() true for -// as long as it is alive and clearing it again in its destructor -- and -// mirrors that lifetime into a local `async_trace_active` flag. A siglongjmp -// out of the guarded callback (Profiler::checkFault() recovering a fault) -// bypasses the AsyncSampleMutex destructor, so walkJavaStack's sigsetjmp -// recovery path explicitly clears is_unwinding_Java() itself whenever that -// flag was set — otherwise it would stay stuck true forever and permanently -// disable async CPU/wall/malloc/socket sampling on that thread (see -// hotspotSupport.cpp). -// -// This gtest binary has no live JVM, so getJavaTraceAsync() itself can't be -// driven into a real fault — it bails out early on a null VMThread. These -// tests instead call the real withAsyncSampleGuard() helper directly (the -// same one walkJavaStack calls) with a real ProfiledThread and a callback -// that faults, via a real sigsetjmp/siglongjmp fault (through the real -// Profiler::segvHandler -> Profiler::checkFault chain), to lock down the -// release behavior. Because the guard/flag lifetime pairing itself lives in -// production code rather than being hand-copied here, a regression in that -// pairing (e.g. async_trace_active being cleared before the mutex's true -// lifetime ended) is caught by these tests. -// --------------------------------------------------------------------------- - -class WalkJavaStackAsyncMutexRecoveryTest : public ::testing::Test { -protected: - void SetUp() override { - ProfiledThread::initCurrentThread(); - _pt = ProfiledThread::current(); - ASSERT_NE(nullptr, _pt); - ASSERT_FALSE(_pt->isProtected()); - ASSERT_FALSE(_pt->is_unwinding_Java()); - - // Force Counters::instance()'s function-local static to construct - // here, off the signal path, before Profiler::segvHandler below can - // fire -- mirrors Profiler::setupSignalHandlers()'s own eager - // warm-up (profiler.cpp), which exists precisely because the first - // touch of the singleton runs non-async-signal-safe - // static-initialization machinery (a C++ guard-variable lock, then - // aligned_alloc/memset; see AGENTS.md's signal-handler-safety rule). - // The recovered fault below reaches Profiler::checkFault(), which - // increments STACKWALK_LONGJMP_RECOVERED, from inside a real SIGSEGV, - // without going through setupSignalHandlers() first. - (void)Counters::getCounters(); - - _orig_segv = OS::replaceSigsegvHandler(Profiler::segvHandler); - _orig_bus = OS::replaceSigbusHandler(Profiler::busHandler); - - _bad_page = mmap(nullptr, 4096, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - ASSERT_NE(MAP_FAILED, _bad_page); - } - - void TearDown() override { - munmap(_bad_page, 4096); - OS::replaceSigsegvHandler(_orig_segv); - OS::replaceSigbusHandler(_orig_bus); - _pt->set_unwinding_Java(false); // keep failures from this test isolated - ProfiledThread::release(); - } - - ProfiledThread* _pt = nullptr; - void* _bad_page = nullptr; - SigAction _orig_segv = nullptr; - SigAction _orig_bus = nullptr; -}; - -// Drives walkJavaStack's cstackgetJmpCtx(); - ASSERT_EQ(nullptr, prev_jmp_buf); - - volatile bool async_trace_active = false; - - if (sigsetjmp(crash_protection_ctx, 1) != 0) { - SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); - _pt->setJmpCtx(prev_jmp_buf); - if (async_trace_active) { - _pt->set_unwinding_Java(false); - } - } else { - _pt->setJmpCtx(&crash_protection_ctx); - - withAsyncSampleGuard(_pt, async_trace_active, [&]() { - EXPECT_TRUE(_pt->is_unwinding_Java()); - - // Simulate a fault inside the guarded callback -- this never - // returns; it lands back at the sigsetjmp above via - // Profiler::checkFault()'s siglongjmp. - *reinterpret_cast(_bad_page) = 1; - FAIL() << "unreachable: the write above must fault"; - }); - FAIL() << "unreachable: withAsyncSampleGuard must not return normally"; - } - - EXPECT_FALSE(_pt->is_unwinding_Java()) - << "a recovered fault must release the AsyncSampleMutex guard, not leak it"; - EXPECT_FALSE(_pt->isProtected()); -} - -// Baseline: a fault that occurs *outside* the AsyncSampleMutex's window -// (async_trace_active still false, as in walkVM()'s own branches) must not -// touch is_unwinding_Java at all -- confirms the recovery reset is scoped to -// the mutex's true lifetime rather than firing unconditionally. -TEST_F(WalkJavaStackAsyncMutexRecoveryTest, RecoveredFaultOutsideGuardWindowLeavesFlagUntouched) { - sigjmp_buf crash_protection_ctx; - sigjmp_buf* prev_jmp_buf = _pt->getJmpCtx(); - volatile bool async_trace_active = false; - - if (sigsetjmp(crash_protection_ctx, 1) != 0) { - SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); - _pt->setJmpCtx(prev_jmp_buf); - if (async_trace_active) { - _pt->set_unwinding_Java(false); - } - } else { - _pt->setJmpCtx(&crash_protection_ctx); - - // walkVM()-style branch: no AsyncSampleMutex involved at all. - *reinterpret_cast(_bad_page) = 1; - FAIL() << "unreachable: the write above must fault"; - } - - EXPECT_FALSE(_pt->is_unwinding_Java()); - EXPECT_FALSE(_pt->isProtected()); -} - #endif // __linux__ From b8f9b17f8f126de86e7a6bd858f3f0e402f96960 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 31 Jul 2026 21:44:26 +0000 Subject: [PATCH 49/51] remove unused --- ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index c87273d4a9..66136be76d 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -1203,15 +1203,6 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { u32 lock_index = request.lock_index; volatile int java_frames = 0; - // True exactly while an AsyncSampleMutex acquired by this call is alive, - // i.e. while ProfiledThread::is_unwinding_Java() is held on our behalf. - // A siglongjmp out of the getJavaTraceAsync() branches below bypasses that - // mutex's destructor, so the recovery path below uses this flag to release - // the per-thread guard itself — otherwise it would stay stuck true forever - // and permanently disable async CPU/wall/malloc/socket sampling on this - // thread. - volatile bool async_trace_active = false; - // walkVM() installs its own sigsetjmp/siglongjmp crash protection (chained // with any pre-existing jmp ctx, see the comment in walkVM), but the // getJavaTraceAsync() path below runs without one: it dereferences @@ -1228,9 +1219,6 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { // segvHandler's SignalHandlerScope destructor. Compensate. SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); prof_thread->setJmpCtx(prev_jmp_buf); - if (async_trace_active) { - prof_thread->set_unwinding_Java(false); - } if (truncated) { *truncated = true; } From 2492825178b23d07ee581e361f5213f13431391e Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sat, 1 Aug 2026 01:11:43 +0200 Subject: [PATCH 50/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index 66136be76d..66057d141a 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -1211,6 +1211,7 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { // walkJavaStack, except HotSpot's AsyncGetCallTrace call, is caught by // Profiler::checkFault() and siglongjmp'd back here instead of crashing the process. ProfiledThread* prof_thread = ProfiledThread::current(); + const bool prev_unwinding_java = prof_thread != nullptr ? prof_thread->is_unwinding_Java() : false; sigjmp_buf crash_protection_ctx; sigjmp_buf* prev_jmp_buf = prof_thread != nullptr ? prof_thread->getJmpCtx() : nullptr; @@ -1219,6 +1220,9 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { // segvHandler's SignalHandlerScope destructor. Compensate. SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); prof_thread->setJmpCtx(prev_jmp_buf); + // A recovered siglongjmp bypasses AsyncSampleMutex destructors, so restore + // the per-thread guard to its pre-walk value. + prof_thread->set_unwinding_Java(prev_unwinding_java); if (truncated) { *truncated = true; } From 371afc575a8a0d853025d45c46cc9e67abc29f01 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Sat, 1 Aug 2026 13:25:27 +0000 Subject: [PATCH 51/51] Enforce memory ordering --- ddprof-lib/src/main/cpp/profiler.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 160706bf5e..60aa659d8a 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -1079,6 +1079,9 @@ void Profiler::setupSignalHandlers() { assert(prof_lib != nullptr); profiler_min_address = reinterpret_cast(prof_lib->minAddress()); profiler_max_address = reinterpret_cast(prof_lib->maxAddress()); + // Prevents the compiler from moving profiler_min_address/profiler_max_address stores pass + // signal handler setup. + std::atomic_signal_fence(std::memory_order_release); #ifdef __FAULT_INJECTION__ // Reserve the PROT_NONE guard region used to poison memory-access sites.