Skip to content
Open
4 changes: 3 additions & 1 deletion ddprof-lib/src/main/cpp/arguments.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,11 @@ Error Arguments::parse(const char *args) {
} else if (strcmp(value, "vm") == 0) {
_cstack = CSTACK_VM;
} else if (strcmp(value, "vmx") == 0) {
// cstack=vmx is a shorthand for cstack=vm,features=mixed
// cstack=vmx is a shorthand for cstack=vm,features=mixed; carrier-frame
// unwinding is enabled automatically since vmx already traverses entry frames
_cstack = CSTACK_VM;
_features.mixed = 1;
_features.carrier_frames = 1;
} else {
_cstack = CSTACK_NO;
}
Expand Down
3 changes: 2 additions & 1 deletion ddprof-lib/src/main/cpp/arguments.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ struct StackWalkFeatures {
unsigned short vtable_target : 1; // show receiver classes of vtable/itable stubs
unsigned short comp_task : 1; // display current compilation task for JIT threads
unsigned short pc_addr : 1; // record exact PC address for each sample
unsigned short _padding : 3; // pad structure to 16 bits
unsigned short carrier_frames: 1; // walk through VT continuation boundary to carrier frames (enabled automatically with cstack=vmx)
unsigned short _padding : 2; // pad structure to 16 bits
};

struct Multiplier {
Expand Down
6 changes: 5 additions & 1 deletion ddprof-lib/src/main/cpp/counters.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@
X(WALKVM_STUB_FRAMESIZE_FALLBACK, "walkvm_stub_framesize_fallback") \
X(WALKVM_FP_CHAIN_ATTEMPT, "walkvm_fp_chain_attempt") \
X(WALKVM_FP_CHAIN_REACHED_CODEHEAP, "walkvm_fp_chain_reached_codeheap") \
X(WALKVM_ANCHOR_NOT_IN_JAVA, "walkvm_anchor_not_in_java") \
X(WALKVM_ANCHOR_NOT_IN_JAVA, "walkvm_anchor_not_in_java") \
X(WALKVM_CONT_BARRIER_HIT, "walkvm_cont_barrier_hit") \
X(WALKVM_ENTER_SPECIAL_HIT, "walkvm_enter_special_hit") \
X(WALKVM_CONT_SPECULATIVE_HIT,"walkvm_cont_speculative_hit") \
X(WALKVM_CONT_ENTRY_NULL, "walkvm_cont_entry_null") \
X(NATIVE_LIBS_DROPPED, "native_libs_dropped") \
X(SIGACTION_PATCHED_LIBS, "sigaction_patched_libs") \
X(SIGACTION_INTERCEPTED, "sigaction_intercepted")
Expand Down
158 changes: 157 additions & 1 deletion ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

#include <cstdlib>
#include <setjmp.h>
#include "asyncSampleMutex.h"
#include "hotspot/hotspotSupport.h"
Expand Down Expand Up @@ -108,6 +109,19 @@ static void fillFrameTypes(ASGCT_CallFrame *frames, int num_frames, VMNMethod *n

static ucontext_t empty_ucontext{};

#ifdef NDEBUG
static const bool CONT_UNWIND_DISABLED = false;
#else
// DEBUG-only: when set, both continuation-unwind detection branches
// (cont_entry_return_pc for fully-thawed VTs, cont_returnBarrier for VTs
// with frozen frames) are skipped, reproducing pre-fix behaviour.
// Used by negative integration tests to verify that carrier frames are not
// visible and walk-error sentinels do appear without the fix.
// NOTE: the env var is evaluated once at library load time; it must be set
// in the environment before the profiler agent is attached.
static const bool CONT_UNWIND_DISABLED = (std::getenv("DDPROF_DISABLE_CONT_UNWIND") != nullptr);
#endif

__attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontext, ASGCT_CallFrame* frames, int max_depth,
StackWalkFeatures features, EventType event_type, int lock_index, bool* truncated) {
if (ucontext == NULL) {
Expand Down Expand Up @@ -176,6 +190,9 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex

const void* prev_native_pc = NULL;

// Last ContinuationEntry crossed; advanced via parent() for nested continuations.
VMContinuationEntry* cont_entry = nullptr;

// Saved anchor data — preserved across anchor consumption so inline
// recovery can redirect even after the anchor pointer has been set to NULL.
// Recovery is one-shot: once attempted, we do not retry to avoid
Expand All @@ -192,6 +209,80 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
anchor = vm_thread->anchor();
}

static const char* CONT_ROOT_FRAME = "JVM Continuation";

// Advances through a continuation boundary to the carrier frame.
// Without carrier_frames (default, cstack=vm): always stops with a "JVM Continuation"
// synthetic root frame — VT frames are complete, carrier internals are noise.
// With carrier_frames (cstack=vmx): attempts to walk through; failures emit BCI_ERROR
// so the sample is truthfully marked truncated.
// Walks cont_entry->parent() on repeated calls to handle nested continuations
// (_parent not triggered by standard single-level VTs today, but required
// once any runtime layers continuations on top of VTs).
//
// all_frames_thawed: true when the bottom VT frame's return PC is
// cont_entry_return_pc (all VT frames are thawed — CPU-bound VT),
// false when it is cont_returnBarrier (frozen frames remain in the
// StackChunk — VT parked and just remounted).
// Needed to derive entry_fp on JDK 21-26 where ContinuationEntry
// type size is absent from vmStructs and contEntry() returns nullptr.
//
// Returns true to continue the walk, false to break.
auto walkThroughContinuation = [&](bool all_frames_thawed) -> bool {
if (depth >= actual_max_depth) return false;
if (!features.carrier_frames) {
fillFrame(frames[depth++], BCI_NATIVE_FRAME, CONT_ROOT_FRAME);
return false;
}

uintptr_t entry_fp;

if (VMContinuationEntry::type_size() > 0) {
// ContinuationEntry is known via vmStructs (JDK 27+, and JDK 21-26
// on distros that expose it). Walk the linked list of entries for
// nested-continuation support and derive the enterSpecial frame FP
// from the struct layout (entry + type_size).
cont_entry = (cont_entry != nullptr) ? cont_entry->parent() : vm_thread->contEntry();
if (cont_entry == nullptr) {
Counters::increment(WALKVM_CONT_ENTRY_NULL);
fillFrame(frames[depth++], BCI_ERROR, "break_cont_entry_null");
return false;
}
entry_fp = cont_entry->entryFP();
} else {
// ContinuationEntry absent from vmStructs (musl/minimal JDK 21-26).
// Derive the enterSpecial frame FP from the current fp:
// all frames thawed (pc == cont_entry_return_pc): fp IS the
// enterSpecial frame FP.
// frozen frames remain (pc == cont_returnBarrier): the saved
// caller FP at *fp leads to the enterSpecial frame on the
// carrier stack.
// Nested continuation tracking is unavailable without type_size().
entry_fp = all_frames_thawed ? fp : (uintptr_t)SafeAccess::load((void**)fp);
}

if (!StackWalkValidation::isValidFP(entry_fp)) {
fillFrame(frames[depth++], BCI_ERROR, "break_cont_entry_fp");
return false;
}
// 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).
uintptr_t carrier_fp = *(uintptr_t*)entry_fp;
const void* carrier_pc = ((const void**)entry_fp)[FRAME_PC_SLOT];
uintptr_t carrier_sp = entry_fp + (FRAME_PC_SLOT + 1) * sizeof(void*);
if (!StackWalkValidation::isValidFP(carrier_fp) ||
StackWalkValidation::inDeadZone(carrier_pc) ||
!StackWalkValidation::isValidSP(carrier_sp, sp, bottom)) {
fillFrame(frames[depth++], BCI_ERROR, "break_cont_carrier_sp");
return false;
}
sp = carrier_sp;
fp = carrier_fp;
pc = carrier_pc;
return true;
};

unwind_loop:

// Walk until the bottom of the stack or until the first Java frame
Expand Down Expand Up @@ -222,8 +313,42 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
break;
}
prev_native_pc = NULL; // we are in JVM code, no previous 'native' PC
// Both continuation boundary PCs are JVM stubs whose findNMethod()
// returns NULL; detect them by exact-PC match before the nmethod
// dispatch below.
// cont_returnBarrier: bottom thawed frame returns here when frozen
// frames remain in the StackChunk (blocking/remounted VT).
// cont_entry_return_pc: bottom thawed frame returns here when the
// continuation is fully thawed (CPU-bound VT, never yielded).
if (!CONT_UNWIND_DISABLED && VMStructs::isContReturnBarrier(pc)) {
Counters::increment(WALKVM_CONT_BARRIER_HIT);
if (walkThroughContinuation(false)) continue;
break;
}
if (!CONT_UNWIND_DISABLED && VMStructs::isContEntryReturnPc(pc)) {
Counters::increment(WALKVM_ENTER_SPECIAL_HIT);
if (walkThroughContinuation(true)) continue;
break;
}
VMNMethod* nm = CodeHeap::findNMethod(pc);
if (nm == NULL) {
// On JDK 21+ builds, the continuation entry PC may be absent
// from vmStructs OR resolved but pointing to the wrong address
// (some distributions expose the symbol at the wrong address, so
// the exact-PC check above never fires). Attempt a fully-thawed
// continuation walk whenever we see an unknown nmethod after
// collecting Java frames. walkThroughContinuation validates the
// fp chain and emits BCI_ERROR cleanly on mismatch, so false
// positives are safe.
if (!CONT_UNWIND_DISABLED
&& features.carrier_frames
&& VM::hotspot_version() >= 21
&& depth > 0
&& vm_thread != NULL && vm_thread->isCarryingVirtualThread()) {
Counters::increment(WALKVM_CONT_SPECULATIVE_HIT);
if (walkThroughContinuation(true)) continue;
break;
}
if (anchor == NULL) {
// Add an error frame only if we cannot recover
fillFrame(frames[depth++], BCI_ERROR, "unknown_nmethod");
Expand All @@ -234,7 +359,14 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
// Always prefer JavaFrameAnchor when it is available,
// since it provides reliable SP and FP.
// Do not treat the topmost stub as Java frame.
if (anchor != NULL && (depth > 0 || !nm->isStub())) {
// Exception: when VT carrier-frame unwinding is active, skip the anchor
// redirect — it can bypass the continuation boundary by jumping directly
// into carrier frames, causing walkThroughContinuation to never fire.
// The continuation mechanism finds carrier frames on its own.
bool anchor_eligible = anchor != NULL && (depth > 0 || !nm->isStub());
bool cont_unwind_active = features.carrier_frames && !CONT_UNWIND_DISABLED
&& vm_thread != NULL && vm_thread->isCarryingVirtualThread();
if (anchor_eligible && !cont_unwind_active) {
Counters::increment(WALKVM_ANCHOR_CONSUMED);
// Preserve anchor data before consumption — getFrame() is read-only
// but we set anchor=NULL below, losing the pointer for later recovery.
Expand All @@ -248,6 +380,10 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
continue; // NMethod has changed as a result of correction
}
anchor = NULL;
} else if (anchor_eligible && cont_unwind_active) {
// Clear the anchor without redirecting so it doesn't corrupt fp
// for the continuation boundary walk.
anchor = NULL;
}

if (nm->isInterpreter()) {
Expand Down Expand Up @@ -297,6 +433,15 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
fillFrame(frames[depth++], BCI_ERROR, "break_interpreted");
break;
} else if (nm->isNMethod()) {
// enterSpecial is a generated native nmethod that acts as the
// continuation entry stub on JDK 27+. It has no JavaCallWrapper, so
// isEntryFrame() will not fire for it. Detect it by identity
// and navigate to the carrier thread via ContinuationEntry.
if (!CONT_UNWIND_DISABLED && nm == VMStructs::enterSpecialNMethod()) {
Counters::increment(WALKVM_ENTER_SPECIAL_HIT);
if (walkThroughContinuation(true)) continue;
break;
}
// Check if deoptimization is in progress before walking compiled frames
if (vm_thread != NULL && vm_thread->inDeopt()) {
fillFrame(frames[depth++], BCI_ERROR, "break_deopt_compiled");
Expand Down Expand Up @@ -963,6 +1108,17 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) {
}
}
}
// 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++;
}
}
}
}
return java_frames;
Expand Down
43 changes: 43 additions & 0 deletions ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ int VMStructs::_narrow_klass_shift = -1;
int VMStructs::_interpreter_frame_bcp_offset = 0;
unsigned char VMStructs::_unsigned5_base = 0;
const void* VMStructs::_call_stub_return = nullptr;
const void* VMStructs::_cont_return_barrier = nullptr;
const void* VMStructs::_cont_entry_return_pc = nullptr;
VMNMethod* VMStructs::_enter_special_nm = nullptr;
const void* VMStructs::_interpreter_start = nullptr;
VMNMethod* VMStructs::_interpreter_nm = nullptr;
const void* VMStructs::_interpreted_frame_valid_start = nullptr;
Expand All @@ -44,6 +47,7 @@ const void* VMStructs::_interpreted_frame_valid_end = nullptr;
// Initialize type size to 0
#define INIT_TYPE_SIZE(name, names) uint64_t VMStructs::TYPE_SIZE_NAME(name) = 0;
DECLARE_TYPES_DO(INIT_TYPE_SIZE)
DECLARE_V21_TYPES_DO(INIT_TYPE_SIZE)
#undef INIT_TYPE_SIZE

#define offset_value -1
Expand All @@ -62,6 +66,7 @@ DECLARE_TYPES_DO(INIT_TYPE_SIZE)
field_type VMStructs::var = field_type##_value;

DECLARE_TYPE_FIELD_DO(DO_NOTHING, INIT_FIELD, INIT_FIELD_WITH_VERSION, DO_NOTHING)
DECLARE_V21_TYPE_FIELD_DO(DO_NOTHING, INIT_FIELD, INIT_FIELD_WITH_VERSION, DO_NOTHING)

#undef INIT_FIELD
#undef INIT_FIELD_WITH_VERSION
Expand Down Expand Up @@ -175,6 +180,7 @@ void VMStructs::init_offsets_and_addresses() {

#define END_TYPE() continue; }
DECLARE_TYPE_FIELD_DO(MATCH_TYPE_NAMES, READ_FIELD_VALUE, READ_FIELD_VALUE_WITH_VERSION, END_TYPE)
DECLARE_V21_TYPE_FIELD_DO(MATCH_TYPE_NAMES, READ_FIELD_VALUE, READ_FIELD_VALUE_WITH_VERSION, END_TYPE)
#undef MATCH_TYPE_NAMES
#undef READ_FIELD_VALUE
#undef READ_FIELD_VALUE_WITH_VERSION
Expand Down Expand Up @@ -205,6 +211,7 @@ void VMStructs::init_type_sizes() {
}

DECLARE_TYPES_DO(READ_TYPE_SIZE)
DECLARE_V21_TYPES_DO(READ_TYPE_SIZE)

#undef READ_TYPE_SIZE

Expand Down Expand Up @@ -271,12 +278,21 @@ void VMStructs::verify_offsets() {
}

// Verify type sizes
// Note: DECLARE_V21_TYPES_DO (VMContinuationEntry) is intentionally excluded here.
// ContinuationEntry is not exported in gHotSpotVMTypes before JDK 27 (added via JDK-8378985);
// asserting type_size() > 0 would SIGABRT on any JDK 21-26 build.
#define VERIFY_TYPE_SIZE(name, names) assert(TYPE_SIZE_NAME(name) > 0);
DECLARE_TYPES_DO(VERIFY_TYPE_SIZE);
#undef VERIFY_TYPE_SIZE


// Verify offsets and addresses
// Note: DECLARE_V21_TYPE_FIELD_DO is intentionally excluded here.
// Continuation-related fields (_cont_entry_offset, _cont_return_barrier_addr,
// _cont_entry_return_pc_addr, _cont_entry_parent_offset) are absent from
// gHotSpotVMStructs in all JDK 21-26 builds: ContinuationEntry was not
// exported in the vmStructs table until JDK 27 (JDK-8378985). walkVM degrades
// gracefully when they are missing.
#define offset_value -1
#define address_value nullptr

Expand Down Expand Up @@ -391,6 +407,25 @@ void VMStructs::resolveOffsets() {
if (_call_stub_return_addr != NULL) {
_call_stub_return = *(const void**)_call_stub_return_addr;
}
if (_cont_return_barrier_addr != NULL) {
_cont_return_barrier = *(const void**)_cont_return_barrier_addr;
}
if (_cont_entry_return_pc_addr != NULL) {
_cont_entry_return_pc = *(const void**)_cont_entry_return_pc_addr;
}
// Fallback for JDK 21-26: StubRoutines::_cont_returnBarrier and
// ContinuationEntry::_return_pc are absent from gHotSpotVMStructs before
// JDK 27 (added via JDK-8378985). Resolve them via C++ symbol lookup.
// Symbol names use Itanium C++ ABI mangling (GCC/Clang), which matches
// the HotSpot build toolchain on all supported platforms.
if (_cont_return_barrier == nullptr && VM::hotspot_version() >= 21) {
const void** sym = (const void**)_libjvm->findSymbol("_ZN12StubRoutines19_cont_returnBarrierE");
if (sym != nullptr) _cont_return_barrier = *sym;
}
if (_cont_entry_return_pc == nullptr && VM::hotspot_version() >= 21) {
const void** sym = (const void**)_libjvm->findSymbol("_ZN17ContinuationEntry10_return_pcE");
if (sym != nullptr) _cont_entry_return_pc = *sym;
}

// Since JDK 23, _metadata_offset is relative to _data_offset. See metadata()
if (_nmethod_immutable_offset < 0) {
Expand Down Expand Up @@ -440,6 +475,14 @@ void VMStructs::resolveOffsets() {
if (_interpreter_nm == NULL && _interpreter_start != NULL) {
_interpreter_nm = CodeHeap::findNMethod(_interpreter_start);
}
if (_enter_special_nm == NULL && _cont_entry_return_pc != NULL) {
// On JDK 27+, enterSpecial is a proper nmethod; findNMethod succeeds.
// On JDK 21-26, it is a RuntimeBlob; findNMethod returns NULL and
// _enter_special_nm stays NULL. The cont_entry_return_pc boundary is
// then detected via isContEntryReturnPc() in the walk loop rather than
// nmethod identity.
_enter_special_nm = CodeHeap::findNMethod(_cont_entry_return_pc);
}
}

void VMStructs::initJvmFunctions() {
Expand Down
Loading
Loading