Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 82 additions & 91 deletions ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1049,23 +1049,24 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames,
}

HotspotStackFrame frame(ucontext);
uintptr_t saved_pc = 0, saved_sp = 0, saved_fp = 0;
// Snapshot pc/sp/fp before this function starts feeding them to HotSpot's
// own AsyncGetCallTrace below (it mutates them in place via frame.restore()
// / frame.unwindStub() / frame.unwindCompiled() to try alternate frames),
// so they can be put back once AGCT is done. Shared with walkJavaStack()'s
// fault-recovery restore -- see StackFrame::RegisterSnapshot.
StackFrame::RegisterSnapshot ctx_snapshot(ucontext);
if (ucontext != NULL) {
saved_pc = frame.pc();
saved_sp = frame.sp();
saved_fp = frame.fp();

if (JitCodeCache::isCallStub((const void *)saved_pc)) {
if (JitCodeCache::isCallStub((const void *)ctx_snapshot.pc())) {
// call_stub is unsafe to walk
frames->bci = BCI_ERROR;
frames->method_id = (jmethodID) "call_stub";
return 1;
}

if (!VMStructs::isSafeToWalk(saved_pc)) {
if (!VMStructs::isSafeToWalk(ctx_snapshot.pc())) {
frames->bci = BCI_NATIVE_FRAME;
CodeBlob *codeBlob =
VMStructs::libjvm()->findBlobByAddress((const void *)saved_pc);
VMStructs::libjvm()->findBlobByAddress((const void *)ctx_snapshot.pc());
if (codeBlob) {
frames->method_id = (jmethodID)codeBlob->_name;
} else {
Expand All @@ -1084,7 +1085,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames,
JVMJavaThreadState state = vm_thread->state();
bool in_java = (state == _thread_in_Java || state == _thread_in_Java_trans);
if (in_java && java_ctx->sp != 0) {
// skip ahead to the Java frames before calling AGCT
// skip ahead to the Java frames before calling AGCT.
frame.restore((uintptr_t)java_ctx->pc, java_ctx->sp, java_ctx->fp);
} else if (state != _thread_uninitialized) {
VMJavaFrameAnchor* a = vm_thread->anchor();
Expand All @@ -1109,7 +1110,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames,
JVMSupport::jvmAsyncGetCallTrace(&trace, max_depth, ucontext);

if (trace.num_frames > 0) {
frame.restore(saved_pc, saved_sp, saved_fp);
ctx_snapshot.restore();
return trace.num_frames;
}

Expand Down Expand Up @@ -1147,7 +1148,7 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames,
trace.frames--;
}
for (int i = 0; trace.num_frames < 0 && i < PROBE_SP_LIMIT; i++) {
frame.sp() += sizeof(void*);
frame.sp() = frame.sp() + sizeof(void*);
JVMSupport::jvmAsyncGetCallTrace(&trace, max_depth, ucontext);
}
}
Expand All @@ -1172,8 +1173,12 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames,
const void* pc = anchor->lastJavaPC();
if (sp != 0 && pc == NULL) {
// We have the last Java frame anchor, but it is not marked as walkable.
// Make it walkable here
pc = ((const void**)sp)[-1];
// Make it walkable here.
// sp comes straight from the anchor with no validation; fault-inject it
// so the unguarded dereference below exercises the sigsetjmp/siglongjmp
// recovery path installed by the caller (walkJavaStack) instead of only
// ever running against a known-good sp.
pc = ((const void**)INJECT_FAULT_ADDRESS_UNLIKELY(sp))[-1];
anchor->setLastJavaPC(pc);

VMNMethod *m = CodeHeap::findNMethod(pc);
Expand Down Expand Up @@ -1215,12 +1220,12 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames,
if (anchor == NULL || anchor->lastJavaSP() == 0) {
// Do not add 'GC_active' for threads with no Java frames, e.g. Compiler
// threads
frame.restore(saved_pc, saved_sp, saved_fp);
ctx_snapshot.restore();
return 0;
}
}

frame.restore(saved_pc, saved_sp, saved_fp);
ctx_snapshot.restore();

if (trace.num_frames > 0) {
return trace.num_frames + (trace.frames - frames);
Expand Down Expand Up @@ -1248,91 +1253,77 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) {
bool* truncated = request.truncated;
u32 lock_index = request.lock_index;

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, except HotSpot's AsyncGetCallTrace call, is caught by
// Profiler::checkFault() and siglongjmp'd back here instead of crashing the process.
ProfiledThread* prof_thread = ProfiledThread::acquireCurrent();
if (prof_thread == nullptr) {
Counters::increment(SAMPLES_DROPPED_THREAD_LOCAL);
return 0;
}
const bool prev_unwinding_java = prof_thread->is_unwinding_Java();
sigjmp_buf crash_protection_ctx;
JmpCtxScope jmp_scope(prof_thread);

if (sigsetjmp(crash_protection_ctx, 1) != 0) {
// checkFault() does a siglongjmp from inside segvHandler, bypassing
// segvHandler's SignalHandlerScope destructor. Compensate.
SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP();
jmp_scope.restore();
// 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;
}
return java_frames;
}
jmp_scope.install(&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) {
// 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, calls into HotSpot's own
// AsyncGetCallTrace, and mutates the real ucontext's pc/sp/fp in place
// while doing so. withUcontextFaultRecovery() installs a jmp ctx around
// both paths, so a SIGSEGV anywhere in this dispatch (except HotSpot's own
// AsyncGetCallTrace call) is caught by Profiler::checkFault() and
// recovered instead of crashing the process -- see its own comment for why
// it also restores the ucontext.
volatile int java_frames = 0;
return withUcontextFaultRecovery(ucontext, prof_thread, truncated, [&]() -> int {
if (features.mixed) {
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);
}
}
}
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++;
}
}
}
} else if (request.event_type == BCI_CPU || request.event_type == BCI_WALL) {
if (cstack >= CSTACK_VM) {
} 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);
}
}
}
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++;
}
}
} 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);
}
}
}
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++;
}
}
}
} 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 {
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++;
}
}
}
}
}

return java_frames;
return java_frames;
}, &java_frames);
}

static void patchClassLoaderData(JNIEnv* jni, jclass klass) {
Expand Down
57 changes: 55 additions & 2 deletions ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@
#ifndef _HOTSPOT_HOTSPOTSUPPORT_H
#define _HOTSPOT_HOTSPOTSUPPORT_H

#include "guards.h"
#include "hotspot/hotspotStackFrame.h"
#include "hotspot/jitCodeCache.h"
#include "frame.h"
#include "stackFrame.h"
#include "stackWalker.h"
#include "threadLocalData.inline.h"

#include <jni.h>
#include <jvmti.h>

class ProfiledThread;
class VMMethod;

class HotspotSupport {
Expand All @@ -38,7 +39,59 @@ class HotspotSupport {
static bool loadMethodIDsIfNeededImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass, bool load_all);
public:
static void initClassloaderInfo(JNIEnv* jni);


// Runs `work` under the ucontext-fault-recovery protocol walkJavaStack()
// needs around getJavaTraceAsync(): installs a sigsetjmp/siglongjmp
// crash-protection scope chained on prof_thread (JmpCtxScope, guards.h),
// and if a SIGSEGV strikes and is recovered by Profiler::checkFault(),
// restores ucontext's pc/sp/fp to what they were before `work` ran.
// getJavaTraceAsync() mutates those registers in place (its pc()/sp()/
// fp() are references into uc_mcontext) and normally restores them
// itself, but a fault mid-mutation (e.g. the PROBE_SP retry loop, or
// inside unwindStub()/unwindCompiled()) skips that restore -- and this
// ucontext is the exact one the kernel uses to resume the sampled
// thread when the signal handler returns.
//
// `partial_result`, if non-null, is the caller's own accumulator for
// whatever `work` has already committed to the output buffer (e.g.
// walkJavaStack's java_frames): getJavaTraceAsync() can fault *after*
// already returning a valid frame count and filling `frames` (e.g. inside
// fillFrameTypes()/isCarryingVirtualThread()'s follow-up work), and that
// partial progress must come back as a truncated-but-valid count rather
// than being discarded as zero frames.
//
// Extracted into one place, rather than hand-rolled separately in
// walkJavaStack(), so production and its regression test invoke the
// identical recovery branch -- see hotspot_crash_protection_ut.cpp's
// WalkJavaStackUcontextRestoreTest. A template rather than
// std::function<int()> so the hot sample path pays no allocation for
// captures.
template <typename Fn>
static int withUcontextFaultRecovery(void* ucontext, ProfiledThread* prof_thread, bool* truncated, Fn&& work, volatile int* partial_result = nullptr) {
const bool prev_unwinding_java = prof_thread->is_unwinding_Java();
StackFrame::RegisterSnapshot ctx_snapshot(ucontext);

sigjmp_buf crash_protection_ctx;
JmpCtxScope jmp_scope(prof_thread);

if (sigsetjmp(crash_protection_ctx, 1) != 0) {
// checkFault() does a siglongjmp from inside segvHandler, bypassing
// segvHandler's SignalHandlerScope destructor. Compensate.
SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP();
jmp_scope.restore();
// 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);
ctx_snapshot.restore();
if (truncated) {
*truncated = true;
}
return partial_result ? *partial_result : 0;
}
jmp_scope.install(&crash_protection_ctx);
return work();
}

static int walkJavaStack(StackWalkRequest& request);
static inline bool canUnwind(const StackFrame& frame, const void*& pc) {
return HotspotStackFrame::unwindAtomicStub(frame, pc);
Expand Down
36 changes: 36 additions & 0 deletions ddprof-lib/src/main/cpp/stackFrame.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,42 @@ class StackFrame {
}
}

// Captures pc/sp/fp for a later restore() -- the null-safe save/restore
// boilerplate needed around code that mutates the real ucontext in place
// (e.g. HotspotSupport::getJavaTraceAsync()'s PROBE_SP loop, or
// unwindStub()/unwindCompiled() writing pc()/sp()/fp() by reference).
// Capturing is a no-op when ucontext is null (pc()/sp()/fp() themselves
// are not null-safe); restore() delegates to StackFrame::restore() above,
// which already is.
//
// Must be restored via an explicit restore() call, never a destructor:
// Profiler::checkFault()'s siglongjmp bypasses destructors, so RAII alone
// can't reach code here -- the same reason JmpCtxScope::restore() must be
// called explicitly (see guards.h).
class RegisterSnapshot {
public:
explicit RegisterSnapshot(void* ucontext) : _ucontext(ucontext) {
if (_ucontext != nullptr) {
StackFrame frame(_ucontext);
_pc = frame.pc();
_sp = frame.sp();
_fp = frame.fp();
}
}

void restore() const {
StackFrame(_ucontext).restore(_pc, _sp, _fp);
}

uintptr_t pc() const { return _pc; }
uintptr_t sp() const { return _sp; }
uintptr_t fp() const { return _fp; }

private:
void* _ucontext;
uintptr_t _pc = 0, _sp = 0, _fp = 0;
};

uintptr_t stackAt(int slot) {
return ((uintptr_t*)sp())[slot];
}
Expand Down
Loading
Loading