diff --git a/ddprof-lib/src/main/cpp/counters.h b/ddprof-lib/src/main/cpp/counters.h
index b81819c222..0ce733f58f 100644
--- a/ddprof-lib/src/main/cpp/counters.h
+++ b/ddprof-lib/src/main/cpp/counters.h
@@ -133,6 +133,7 @@
X(SAMPLES_DROPPED_THREAD_LOCAL, "samples_dropped_thread_local") \
X(SAFECOPY_FAILED, "safecopy_failed") \
X(SAFEFETCH_FAILED, "safefetch_failed") \
+ X(SAFESTORE_FAILED, "safestore_failed") \
/* Every siglongjmp recovery, from any protected window, counted centrally \
* in Profiler::checkFault(). */ \
X(STACKWALK_LONGJMP_RECOVERED, "stackwalk_longjmp_recovered") \
@@ -196,7 +197,8 @@
#ifdef DEBUG
#define DD_COUNTER_TABLE_DEBUG(X) \
X(SAFEFETCH_WHILE_PROTECTED, "safefetch_while_protected") \
- X(SAFECOPY_WHILE_PROTECTED, "safecopy_while_protected")
+ X(SAFECOPY_WHILE_PROTECTED, "safecopy_while_protected") \
+ X(SAFESTORE_WHILE_PROTECTED, "safestore_while_protected")
#else
#define DD_COUNTER_TABLE_DEBUG(X)
#endif
diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotStackFrame.h b/ddprof-lib/src/main/cpp/hotspot/hotspotStackFrame.h
index b631e9077f..044d6a29ec 100644
--- a/ddprof-lib/src/main/cpp/hotspot/hotspotStackFrame.h
+++ b/ddprof-lib/src/main/cpp/hotspot/hotspotStackFrame.h
@@ -15,6 +15,47 @@ class HotspotStackFrame : public StackFrame {
explicit HotspotStackFrame(void* ucontext): StackFrame(ucontext) {
}
+ class RegisterSnapshot : public StackFrame::RegisterSnapshot {
+ private:
+ // volatile: saveJavaAnchor() mutates these (called from
+ // getJavaTraceAsync(), reached through
+ // HotspotSupport::withUcontextFaultRecovery()'s work(ctx_snapshot))
+ // between that function's sigsetjmp() and a possible siglongjmp()
+ // out of a recovered fault; restore() then reads them back at the
+ // landing pad to decide whether/how to restore the JavaThread
+ // anchor. Per the setjmp/longjmp rules (C11 7.13.2.1p3, inherited
+ // by C++), a non-volatile automatic local modified in that window
+ // has an indeterminate value after longjmp -- the same hazard
+ // ResolvedNames::_long_method_name (hotspotSupport.cpp) guards
+ // against for an analogous fault-recovery readback.
+ VMJavaFrameAnchor* volatile _anchor;
+ const void* volatile _anchor_pc;
+ public:
+ explicit RegisterSnapshot(void* ucontext) : StackFrame::RegisterSnapshot(ucontext),
+ _anchor(nullptr), _anchor_pc(nullptr) {
+ }
+
+ virtual ~RegisterSnapshot() {
+ restore();
+ }
+
+ void saveJavaAnchor(VMJavaFrameAnchor* anchor, const void* pc) {
+ assert(anchor != nullptr);
+ _anchor = anchor;
+ _anchor_pc = pc;
+ }
+
+ virtual void restore() override {
+ StackFrame::RegisterSnapshot::restore();
+ if (_anchor != nullptr) {
+ // Safe store, cannot fault
+ bool ret = _anchor->setLastJavaPC(_anchor_pc);
+ assert(ret && "Failed to restore lastJavaPC");
+ _anchor = nullptr;
+ }
+ }
+ };
+
bool unwindCompiled(VMNMethod* nm) {
return unwindCompiled(nm, pc(), sp(), fp());
}
diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp
index 910dc7006f..423f6c9f81 100644
--- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp
+++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp
@@ -1031,7 +1031,7 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames,
int max_depth, StackContext *java_ctx,
- bool *truncated) {
+ bool *truncated, HotspotStackFrame::RegisterSnapshot& ctx_snapshot) {
// Workaround for JDK-8132510: it's not safe to call GetEnv() inside a signal
// handler since JDK 9, so we do it only for threads already registered in
// ThreadLocalStorage
@@ -1049,23 +1049,30 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames,
}
HotspotStackFrame frame(ucontext);
- uintptr_t saved_pc = 0, saved_sp = 0, saved_fp = 0;
+ // ctx_snapshot (passed in by the caller) snapshotted 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. It's the same instance withUcontextFaultRecovery()
+ // restores on a recovered fault -- see its own comment -- which is also why
+ // any JavaThread anchor mutation below must be recorded on it via
+ // saveJavaAnchor() rather than tracked locally. None of this function's own
+ // return paths need to call ctx_snapshot.restore() themselves: it's a local
+ // of withUcontextFaultRecovery, and RegisterSnapshot's destructor
+ // unconditionally restores on scope exit -- covering every return here,
+ // the same way it covers a recovered fault.
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 {
@@ -1084,7 +1091,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();
@@ -1103,13 +1110,11 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames,
return 0;
}
- JitWriteProtection jit(false);
// AsyncGetCallTrace writes to ASGCT_CallFrame array
ASGCT_CallTrace trace = {jni, 0, frames};
JVMSupport::jvmAsyncGetCallTrace(&trace, max_depth, ucontext);
if (trace.num_frames > 0) {
- frame.restore(saved_pc, saved_sp, saved_fp);
return trace.num_frames;
}
@@ -1147,7 +1152,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);
}
}
@@ -1167,14 +1172,21 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames,
} else if (trace.num_frames == ticks_unknown_not_Java &&
!(safe_mode & LAST_JAVA_PC)) {
VMJavaFrameAnchor* anchor = vm_thread->anchor();
- if (anchor == NULL) return 0;
+ if (anchor == NULL) {
+ return 0;
+ }
uintptr_t sp = anchor->lastJavaSP();
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];
- anchor->setLastJavaPC(pc);
+ // 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 (withUcontextFaultRecovery) instead of only
+ // ever running against a known-good sp.
+ pc = *(const void**)INJECT_FAULT_ADDRESS_UNLIKELY((const void**)sp - 1);
+ ctx_snapshot.saveJavaAnchor(anchor, NULL);
+ anchor->setLastJavaPC(pc);
VMNMethod *m = CodeHeap::findNMethod(pc);
const Libraries* libs = Profiler::instance()->libraries();
@@ -1191,13 +1203,13 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames,
} else if (libs->findLibraryByAddress(pc) != NULL) {
JVMSupport::jvmAsyncGetCallTrace(&trace, max_depth, ucontext);
}
-
- anchor->setLastJavaPC(nullptr);
}
} else if (trace.num_frames == ticks_not_walkable_not_Java &&
!(safe_mode & LAST_JAVA_PC)) {
VMJavaFrameAnchor* anchor = vm_thread->anchor();
- if (anchor == NULL) return 0;
+ if (anchor == NULL) {
+ return 0;
+ }
uintptr_t sp = anchor->lastJavaSP();
const void* pc = anchor->lastJavaPC();
if (sp != 0 && pc != NULL) {
@@ -1215,13 +1227,10 @@ 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);
return 0;
}
}
- frame.restore(saved_pc, saved_sp, saved_fp);
-
if (trace.num_frames > 0) {
return trace.num_frames + (trace.frames - frames);
}
@@ -1238,6 +1247,48 @@ int HotspotSupport::getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames,
return trace.frames - frames + 1;
}
+int HotspotSupport::asyncJavaTraceWithPostProcessing(void* ucontext, ASGCT_CallFrame* frames,
+ int max_depth, StackContext* java_ctx,
+ bool* truncated, ProfiledThread* prof_thread) {
+ // getJavaTraceAsync() dereferences VMThread/anchor state directly, calls
+ // into HotSpot's own AsyncGetCallTrace, and mutates the real ucontext's
+ // pc/sp/fp (and, via saveJavaAnchor(), the JavaThread anchor) in place
+ // while doing so, with no crash protection of its own. withUcontextFaultRecovery()
+ // installs a jmp ctx around just that path, so a SIGSEGV there (except
+ // inside 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.
+ // Both guards deliberately outlive withUcontextFaultRecovery(): a recovered
+ // siglongjmp bypasses destructors for objects in its callback. Keeping the
+ // JIT protection guard here ensures macOS arm64 restores the sampled
+ // thread's W^X state when this function returns from the recovery branch.
+ // WxRestoreOnRecoveryTest pins this guard layout on macOS arm64.
+ AsyncSampleMutex mutex(prof_thread);
+ if (!mutex.acquired()) {
+ return 0;
+ }
+ JitWriteProtection jit(false);
+ volatile int partial = 0;
+ return withUcontextFaultRecovery(ucontext, prof_thread, truncated, partial, [&](HotspotStackFrame::RegisterSnapshot& ctx_snapshot) {
+ partial = getJavaTraceAsync(ucontext, frames, max_depth, java_ctx, truncated, ctx_snapshot);
+ if (partial > 0 && java_ctx->pc != NULL && VMStructs::hasMethodStructs()) {
+ VMNMethod* nmethod = CodeHeap::findNMethod(java_ctx->pc);
+ if (nmethod != NULL) {
+ fillFrameTypes(frames, partial, nmethod);
+ }
+ }
+ if (partial > 0 && VM::hotspot_version() >= 21 && partial < max_depth) {
+ VMThread* carrier = VMThread::current();
+ if (carrier != nullptr && carrier->isCarryingVirtualThread()) {
+ frames[partial].bci = BCI_NATIVE_FRAME;
+ frames[partial].method_id = (jmethodID) "JVM Continuation";
+ LP64_ONLY(frames[partial].padding = 0;)
+ partial++;
+ }
+ }
+ });
+}
+
int HotspotSupport::walkJavaStack(StackWalkRequest& request) {
CStack cstack = Profiler::instance()->cstackMode();
StackWalkFeatures features = Profiler::instance()->stackWalkFeatures();
@@ -1248,91 +1299,37 @@ 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);
+ // walkVM() mutates none of the real ucontext's pc/sp/fp -- it unwinds
+ // through its own local pc/sp/fp copies -- and installs its own
+ // sigsetjmp/siglongjmp crash protection (chained with any pre-existing jmp
+ // ctx, see the comment in walkVM), so a fault during it is caught by
+ // walkVM's own recovery branch, never propagated here. It's dispatched
+ // directly, with no withUcontextFaultRecovery() wrapper: constructing that
+ // wrapper's RegisterSnapshot would be pure overhead on this path -- pc/sp/fp
+ // reads into uc_mcontext with nothing to ever restore. asyncJavaTraceWithPostProcessing()
+ // (see its own comment) is the getJavaTraceAsync() counterpart, wrapped
+ // because that path does mutate the ucontext with no crash protection of
+ // its own.
+ //
+ // isHookPrefixedSample()/BCI_CPU/BCI_WALL samples share the exact same
+ // walkVM-vs-async dispatch, just gated on different event types, so they're
+ // collapsed into one branch here rather than duplicated per event type.
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);
- }
- }
- }
- 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) {
+ return walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated);
+ }
+ if (isHookPrefixedSample(request.event_type) || 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 walkVM(ucontext, frames, max_depth, features, eventTypeFromBCI(request.event_type), lock_index, truncated);
}
+ return asyncJavaTraceWithPostProcessing(ucontext, frames, max_depth, java_ctx, truncated, prof_thread);
}
-
- return java_frames;
+ return 0;
}
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 7617403fac..4c52d70219 100644
--- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h
+++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h
@@ -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
#include
-class ProfiledThread;
class VMMethod;
class HotspotSupport {
@@ -31,14 +32,114 @@ class HotspotSupport {
StackWalkFeatures features, EventType event_type,
int lock_index, bool* truncated = nullptr);
+ // ctx_snapshot is owned by the caller's withUcontextFaultRecovery() scope
+ // (hotspotSupport.cpp), not constructed locally: getJavaTraceAsync() must
+ // record the JavaThread anchor mutation it's about to make (see
+ // saveJavaAnchor() below) on the exact same RegisterSnapshot instance
+ // whose restore() runs on a recovered SIGSEGV, or the anchor never gets
+ // restored -- a fault siglongjmps past this whole function's frame,
+ // skipping any local snapshot it might otherwise have owned.
static int getJavaTraceAsync(void *ucontext, ASGCT_CallFrame *frames,
int max_depth, StackContext *java_ctx,
- bool *truncated);
+ bool *truncated, HotspotStackFrame::RegisterSnapshot& ctx_snapshot);
static bool loadMethodIDsIfNeededImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass, bool load_all);
+
+ // Runs getJavaTraceAsync() under withUcontextFaultRecovery(), then layers
+ // on its two post-processing steps: resolving frame types for the top
+ // Java frame via fillFrameTypes(), and appending a synthetic "JVM
+ // Continuation" frame when the sampled thread is carrying a virtual
+ // thread. Called from walkJavaStack()'s single dispatch branch that
+ // reaches getJavaTraceAsync() (the hook-prefixed/BCI_CPU/BCI_WALL arm
+ // with cstack < CSTACK_VM) -- previously duplicated verbatim in each
+ // of the two dispatch arms.
+ static int asyncJavaTraceWithPostProcessing(void* ucontext, ASGCT_CallFrame* frames,
+ int max_depth, StackContext* java_ctx,
+ bool* truncated, ProfiledThread* prof_thread);
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` 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). `work` is
+ // void-returning and writes only into `partial_result` (by capturing it,
+ // not by a return value) -- the wrapper reads `partial_result` back on
+ // both the normal-completion path and the recovery path, so there is a
+ // single channel for the result rather than two that a caller could wire
+ // to different objects, or forget to wire at all (a plain return value
+ // whose default became 0 on a recovered fault was exactly the
+ // "recovered fault discards an already-valid partial trace" shape this
+ // wrapper exists to avoid). The caller must initialize `partial_result`
+ // itself before the call (normally to 0); before this recovery logic was
+ // extracted into a shared helper, walkJavaStack() already reported the
+ // fault-after-partial-progress case as a truncated-but-valid count by
+ // reading back its own `volatile int java_frames` local from the recovery
+ // branch -- this parameter is how that pre-existing behavior is preserved
+ // now that the branch lives here instead, not a new fix on top of it.
+ //
+ // `work` receives ctx_snapshot by reference so that getJavaTraceAsync()
+ // (via its own saveJavaAnchor() call, above) can record a JavaThread
+ // anchor mutation on this exact instance -- the one whose restore()
+ // actually runs below on a recovered fault. A local snapshot inside
+ // getJavaTraceAsync() would be useless: a siglongjmp from a fault there
+ // jumps straight back to this sigsetjmp, skipping getJavaTraceAsync's
+ // entire stack frame (and anything it owned) without running any of its
+ // code.
+ //
+ // 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 so the hot sample path pays no allocation for
+ // captures. Must stay defined here (not in hotspotSupport.cpp): as a
+ // template, its body needs to be visible wherever it's instantiated --
+ // both walkJavaStack() and the regression test's own call sites, which
+ // each pass a distinct closure type.
+ template
+ static int withUcontextFaultRecovery(void* ucontext, ProfiledThread* prof_thread, bool* truncated, volatile int& partial_result, Fn&& work) {
+ const bool prev_unwinding_java = prof_thread->is_unwinding_Java();
+ HotspotStackFrame::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();
+ // Backstop for template callers whose work() acquires the
+ // AsyncSampleMutex itself (a recovered siglongjmp bypasses its
+ // destructor). The production caller constructs the mutex above
+ // this scope (see asyncJavaTraceWithPostProcessing), where this
+ // restore is a no-op and the mutex destructor clears the flag
+ // on return.
+ prof_thread->set_unwinding_Java(prev_unwinding_java);
+ ctx_snapshot.restore();
+ if (truncated) {
+ *truncated = true;
+ }
+ return partial_result;
+ }
+ jmp_scope.install(&crash_protection_ctx);
+ work(ctx_snapshot);
+ return partial_result;
+ }
+
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/hotspot/vmStructs.h b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h
index 38299aba3c..c61ccbc3b3 100644
--- a/ddprof-lib/src/main/cpp/hotspot/vmStructs.h
+++ b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h
@@ -737,22 +737,28 @@ DECLARE(VMJavaFrameAnchor)
NOADDRSANITIZE uintptr_t lastJavaSP() {
assert(_anchor_sp_offset >= 0);
- return (uintptr_t) SafeAccess::loadPtr((void**) at(_anchor_sp_offset), nullptr);
+ return (uintptr_t) *(void**) at(_anchor_sp_offset);
}
NOADDRSANITIZE uintptr_t lastJavaFP() {
assert(_anchor_fp_offset >= 0);
- return (uintptr_t) SafeAccess::loadPtr((void**) at(_anchor_fp_offset), nullptr);
+ return (uintptr_t) *(void**) at(_anchor_fp_offset);
}
NOADDRSANITIZE const void* lastJavaPC() {
assert(_anchor_pc_offset >= 0);
- return SafeAccess::loadPtr((void**) at(_anchor_pc_offset), nullptr);
+ return *(void**) at(_anchor_pc_offset);
}
- void setLastJavaPC(const void* pc) {
+ template
+ bool setLastJavaPC(const void* pc) {
assert(_anchor_pc_offset >= 0);
- *(const void**) at(_anchor_pc_offset) = pc;
+ if (SafeStore) {
+ return SafeAccess::storePtr((void**)at(_anchor_pc_offset), (void*)pc);
+ } else {
+ *(const void**) at(_anchor_pc_offset) = pc;
+ return true;
+ }
}
NOADDRSANITIZE bool getFrame(const void*& pc, uintptr_t& sp, uintptr_t& fp) {
diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp
index 576c2555f8..26761e99e5 100644
--- a/ddprof-lib/src/main/cpp/profiler.cpp
+++ b/ddprof-lib/src/main/cpp/profiler.cpp
@@ -1106,8 +1106,8 @@ void Profiler::setupSignalHandlers() {
// 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
- // STACKWALK* counters. The first touch of the singleton lazily runs
+ // SAFEFETCH_FAILED / SAFESTORE_FAILED / SAFECOPY_FAILED, and other async
+ // handlers bump the 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
diff --git a/ddprof-lib/src/main/cpp/safeAccess.cpp b/ddprof-lib/src/main/cpp/safeAccess.cpp
index c6f3bbbd89..87eea0557a 100644
--- a/ddprof-lib/src/main/cpp/safeAccess.cpp
+++ b/ddprof-lib/src/main/cpp/safeAccess.cpp
@@ -28,6 +28,8 @@
extern "C" int safefetch32_cont(int* adr, int errValue);
extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue);
+extern "C" int safestore32_cont(int* adr, int value);
+extern "C" int64_t safestore64_cont(int64_t* adr, int64_t value);
// safecopy_impl copies `len` bytes from `src` to `dst` one byte at a time and
// returns 1 on success. If any load from `src` faults, the signal handler
@@ -80,6 +82,12 @@ static void verify_safecopy_range() {
The load is protected by the 'handle_safefetch` signal handler, who sets next `pc`
to `safefetch32_cont/safefetch64_cont`, upon returning from signal handler,
`safefetch32_cont/safefetch64_cont` returns `errValue`
+
+ Storing a 32-bit/64-bit value to a specific address works the same way, in
+ reverse: `safestore32_impl`/`safestore64_impl` write `value` to `adr` and
+ return 1. If that write faults, `handle_safefetch` redirects to
+ `safestore32_cont`/`safestore64_cont`, which return 0 instead -- so the
+ caller can tell a successful store from one that never happened.
**/
#if defined(__x86_64__)
#ifdef __APPLE__
@@ -104,6 +112,28 @@ static void verify_safecopy_range() {
_safefetch64_cont:
movq %rsi, %rax
ret
+ .globl _safestore32_impl
+ .private_extern _safestore32_impl
+ _safestore32_impl:
+ movl %esi, (%rdi)
+ movl $1, %eax
+ ret
+ .globl _safestore32_cont
+ .private_extern _safestore32_cont
+ _safestore32_cont:
+ xorl %eax, %eax
+ ret
+ .globl _safestore64_impl
+ .private_extern _safestore64_impl
+ _safestore64_impl:
+ movq %rsi, (%rdi)
+ movl $1, %eax
+ ret
+ .globl _safestore64_cont
+ .private_extern _safestore64_cont
+ _safestore64_cont:
+ xorl %eax, %eax
+ ret
.globl _safecopy_impl
.private_extern _safecopy_impl
_safecopy_impl:
@@ -151,6 +181,32 @@ static void verify_safecopy_range() {
safefetch64_cont:
movq %rsi, %rax
ret
+ .globl safestore32_impl
+ .hidden safestore32_impl
+ .type safestore32_impl, %function
+ safestore32_impl:
+ movl %esi, (%rdi)
+ movl $1, %eax
+ ret
+ .globl safestore32_cont
+ .hidden safestore32_cont
+ .type safestore32_cont, %function
+ safestore32_cont:
+ xorl %eax, %eax
+ ret
+ .globl safestore64_impl
+ .hidden safestore64_impl
+ .type safestore64_impl, %function
+ safestore64_impl:
+ movq %rsi, (%rdi)
+ movl $1, %eax
+ ret
+ .globl safestore64_cont
+ .hidden safestore64_cont
+ .type safestore64_cont, %function
+ safestore64_cont:
+ xorl %eax, %eax
+ ret
.globl safecopy_impl
.hidden safecopy_impl
.type safecopy_impl, %function
@@ -197,6 +253,28 @@ static void verify_safecopy_range() {
_safefetch64_cont:
mov x0, x1
ret
+ .globl _safestore32_impl
+ .private_extern _safestore32_impl
+ _safestore32_impl:
+ str w1, [x0]
+ mov w0, #1
+ ret
+ .globl _safestore32_cont
+ .private_extern _safestore32_cont
+ _safestore32_cont:
+ mov w0, #0
+ ret
+ .globl _safestore64_impl
+ .private_extern _safestore64_impl
+ _safestore64_impl:
+ str x1, [x0]
+ mov w0, #1
+ ret
+ .globl _safestore64_cont
+ .private_extern _safestore64_cont
+ _safestore64_cont:
+ mov w0, #0
+ ret
.globl _safecopy_impl
.private_extern _safecopy_impl
_safecopy_impl:
@@ -244,6 +322,32 @@ static void verify_safecopy_range() {
safefetch64_cont:
mov x0, x1
ret
+ .globl safestore32_impl
+ .hidden safestore32_impl
+ .type safestore32_impl, %function
+ safestore32_impl:
+ str w1, [x0]
+ mov w0, #1
+ ret
+ .globl safestore32_cont
+ .hidden safestore32_cont
+ .type safestore32_cont, %function
+ safestore32_cont:
+ mov w0, #0
+ ret
+ .globl safestore64_impl
+ .hidden safestore64_impl
+ .type safestore64_impl, %function
+ safestore64_impl:
+ str x1, [x0]
+ mov w0, #1
+ ret
+ .globl safestore64_cont
+ .hidden safestore64_cont
+ .type safestore64_cont, %function
+ safestore64_cont:
+ mov w0, #0
+ ret
.globl safecopy_impl
.hidden safecopy_impl
.type safecopy_impl, %function
@@ -270,18 +374,23 @@ static void verify_safecopy_range() {
#endif
#ifdef DEBUG
-void SafeAccess::countIfLongjmpProtected(bool isCopy) {
+void SafeAccess::countIfLongjmpProtected(SafeAccessKind kind) {
ProfiledThread* t = ProfiledThread::current(); // never allocates
if (t != nullptr && t->isProtected()) {
- Counters::increment(isCopy ? SAFECOPY_WHILE_PROTECTED
- : SAFEFETCH_WHILE_PROTECTED);
+ CounterId id;
+ switch (kind) {
+ case SafeAccessKind::Fetch: id = SAFEFETCH_WHILE_PROTECTED; break;
+ case SafeAccessKind::Store: id = SAFESTORE_WHILE_PROTECTED; break;
+ case SafeAccessKind::Copy: id = SAFECOPY_WHILE_PROTECTED; break;
+ }
+ Counters::increment(id);
}
}
#endif
bool SafeAccess::safeCopy(void* dst, const void* src, size_t len) {
#ifdef DEBUG
- countIfLongjmpProtected(true);
+ countIfLongjmpProtected(SafeAccessKind::Copy);
#endif
// The copy runs entirely inside the safecopy_impl assembly stub, which
// reads `src` one byte at a time. If a load faults, handle_safefetch
@@ -308,6 +417,14 @@ bool SafeAccess::handle_safefetch(int sig, void* context) {
uc->current_pc = (uintptr_t)safefetch64_cont;
Counters::increment(SAFEFETCH_FAILED);
return true;
+ } else if (pc == (uintptr_t)safestore32_impl) {
+ uc->current_pc = (uintptr_t)safestore32_cont;
+ Counters::increment(SAFESTORE_FAILED);
+ return true;
+ } else if (pc == (uintptr_t)safestore64_impl) {
+ uc->current_pc = (uintptr_t)safestore64_cont;
+ Counters::increment(SAFESTORE_FAILED);
+ return true;
} else if (pc >= (uintptr_t)safecopy_impl && pc < (uintptr_t)safecopy_cont) {
// Unlike safefetch, the faulting load can be at any pc inside the copy
// loop, so match the whole [safecopy_impl, safecopy_cont) range.
@@ -327,7 +444,7 @@ void* SafeAccess::load(void** ptr, void* default_value) {
int32_t SafeAccess::load32(int32_t* ptr, int32_t default_value) {
#ifdef DEBUG
- countIfLongjmpProtected(false);
+ countIfLongjmpProtected(SafeAccessKind::Fetch);
#endif
int res = safefetch32_impl((int*)ptr, (int)default_value);
return static_cast(res);
@@ -335,7 +452,7 @@ int32_t SafeAccess::load32(int32_t* ptr, int32_t default_value) {
void* SafeAccess::loadPtr(void** ptr, void* default_value) {
#ifdef DEBUG
- countIfLongjmpProtected(false);
+ countIfLongjmpProtected(SafeAccessKind::Fetch);
#endif
#if defined(__x86_64__) || defined(__aarch64__)
int64_t res = safefetch64_impl((int64_t*)ptr, (int64_t)reinterpret_cast(default_value));
@@ -346,3 +463,31 @@ void* SafeAccess::loadPtr(void** ptr, void* default_value) {
#endif
return *ptr;
}
+
+// NOINLINE implementations using safestore infrastructure -- write-side
+// counterpart to load/load32/loadPtr above. Same stable-address requirement:
+// handle_safefetch() matches faults by the exact pc of safestore32_impl /
+// safestore64_impl, so these must not be inlined into their callers.
+bool SafeAccess::store(void** ptr, void* value) {
+ return storePtr(ptr, value);
+}
+
+bool SafeAccess::store32(int32_t* ptr, int32_t value) {
+#ifdef DEBUG
+ countIfLongjmpProtected(SafeAccessKind::Store);
+#endif
+ return safestore32_impl((int*)ptr, (int)value) != 0;
+}
+
+bool SafeAccess::storePtr(void** ptr, void* value) {
+#ifdef DEBUG
+ countIfLongjmpProtected(SafeAccessKind::Store);
+#endif
+#if defined(__x86_64__) || defined(__aarch64__)
+ return safestore64_impl((int64_t*)ptr, (int64_t)reinterpret_cast(value)) != 0;
+#elif defined(__i386__) || defined(__arm__) || defined(__thumb__)
+ return safestore32_impl((int*)ptr, (int)reinterpret_cast(value)) != 0;
+#endif
+ *ptr = value;
+ return true;
+}
diff --git a/ddprof-lib/src/main/cpp/safeAccess.h b/ddprof-lib/src/main/cpp/safeAccess.h
index 564743b153..63deb6c6f8 100644
--- a/ddprof-lib/src/main/cpp/safeAccess.h
+++ b/ddprof-lib/src/main/cpp/safeAccess.h
@@ -26,6 +26,8 @@
extern "C" int safefetch32_impl(int* adr, int errValue);
extern "C" int64_t safefetch64_impl(int64_t* adr, int64_t errValue);
+extern "C" int safestore32_impl(int* adr, int value);
+extern "C" int64_t safestore64_impl(int64_t* adr, int64_t value);
#ifdef __clang__
#define NOINLINE __attribute__((noinline))
@@ -53,7 +55,7 @@ class SafeAccess {
NOINLINE
static int safeFetch32(int* ptr, int errorValue) {
#ifdef DEBUG
- countIfLongjmpProtected(false);
+ countIfLongjmpProtected(SafeAccessKind::Fetch);
#endif
return safefetch32_impl(ptr, errorValue);
}
@@ -66,7 +68,7 @@ class SafeAccess {
NOINLINE
static int64_t safeFetch64(int64_t* ptr, int64_t errorValue) {
#ifdef DEBUG
- countIfLongjmpProtected(false);
+ countIfLongjmpProtected(SafeAccessKind::Fetch);
#endif
return safefetch64_impl(ptr, errorValue);
}
@@ -92,6 +94,32 @@ class SafeAccess {
NOINLINE __attribute__((aligned(16)))
static void *loadPtr(void** ptr, void* default_value);
+ /**
+ * Safely writes a 32-bit value to the given address.
+ *
+ * CRITICAL: This function MUST NOT be inlined. See safeFetch32 for why --
+ * the same handle_safefetch fault-redirect relies on the store happening at
+ * this function's own stable address.
+ *
+ * ptr: Address to write to (may be invalid)
+ * value: Value to store at ptr
+ * return true if the store succeeded, false if the write faulted
+ */
+ NOINLINE __attribute__((aligned(16)))
+ static bool store32(int32_t* ptr, int32_t value);
+
+ /**
+ * Safely writes a pointer-sized value to the given address. See store32 for
+ * details.
+ */
+ NOINLINE __attribute__((aligned(16)))
+ static bool storePtr(void** ptr, void* value);
+
+ // NOINLINE function with a stable address for JVM patching (vmStructs.cpp),
+ // mirroring load(): a void*-typed convenience wrapper around storePtr().
+ NOINLINE __attribute__((aligned(16)))
+ static bool store(void** ptr, void* value);
+
static inline bool isReadable(const void* ptr) {
return load32((int32_t*)ptr, 1) != 1 ||
load32((int32_t*)ptr, -1) != -1;
@@ -118,13 +146,15 @@ 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 siglongjmp-protected region, where
- // the safefetch/safecopy overhead is redundant (a fault there is caught by the
- // 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);
+ // Debug diagnostic: bump a counter when a SafeAccess read/write/copy is issued
+ // while the current thread is already inside a walkVM siglongjmp-protected
+ // region, where the safefetch/safestore/safecopy overhead is redundant (a
+ // fault there is caught by the siglongjmp anyway). Defined out-of-line in
+ // safeAccess.cpp so this widely-included header need not pull in
+ // threadLocalData.h / counters.h -- kind is a self-contained enum rather
+ // than a CounterId for the same reason.
+ enum class SafeAccessKind { Fetch, Store, Copy };
+ static void countIfLongjmpProtected(SafeAccessKind kind);
#endif
};
diff --git a/ddprof-lib/src/main/cpp/stackFrame.h b/ddprof-lib/src/main/cpp/stackFrame.h
index 83bd24d661..bc3834f9ae 100644
--- a/ddprof-lib/src/main/cpp/stackFrame.h
+++ b/ddprof-lib/src/main/cpp/stackFrame.h
@@ -33,6 +33,45 @@ 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 on recovery path.
+ // Profiler::checkFault()'s siglongjmp bypasses destructors, so RAII alone
+ // can't reach code here -- the same reason JmpCtxScope::restore() must be
+ // called explicitly on recovery path (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();
+ }
+ }
+ virtual ~RegisterSnapshot() {
+ restore();
+ }
+
+ virtual void restore() {
+ 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];
}
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 cba11fad81..86074b3b66 100644
--- a/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp
+++ b/ddprof-lib/src/test/cpp/hotspot_crash_protection_ut.cpp
@@ -27,8 +27,12 @@
* C. sigjmp_buf chaining across nested/interrupted walkVM() calls
* C2. JmpCtxScope, the RAII form of that protocol (used by
* HotspotSupport::resolve())
- * F. HotspotSupport::walkJavaStack()'s AsyncSampleMutex release on a
- * recovered fault
+ * D. Profiler::checkFault() guard clauses
+ * E. VTable-stub null-klass and safeFetch64==0 TOCTOU guards
+ * G. HotspotSupport::walkJavaStack()'s ucontext restore on a recovered
+ * fault
+ * H. macOS arm64: JitWriteProtection's W^X (APCTL) restore when its guard
+ * outlives the recovered withUcontextFaultRecovery() call
*/
#include
@@ -40,6 +44,16 @@
#include "jvmThread.h"
#include "safeAccess.h"
#include "os.h"
+#include "stackFrame.h"
+#include "hotspot/hotspotSupport.h"
+// HotspotStackFrame::RegisterSnapshot::restore() (constructed inside
+// withUcontextFaultRecovery(), instantiated here) conditionally calls
+// VMJavaFrameAnchor::setLastJavaPC(), which is inline in vmStructs.h but
+// bottoms out in VMStructs::at()/crashProtectionActive(), defined in
+// vmStructs.inline.h. Without this include, assertion-enabled builds (the
+// gtest targets) leave those symbols unresolved at link time -- see the same
+// note in hotspotStackFrame_aarch64.cpp.
+#include "hotspot/vmStructs.inline.h"
#ifdef __linux__
@@ -47,6 +61,19 @@
#include
#include
+// Test-only friend accessor for VMStructs' protected static offsets (see the
+// `friend class VMStructsTestAccessor;` declaration in vmStructs.h) -- lets
+// FaultInsideProfilerRangeRecoversAndRestoresJavaThreadAnchor below fake a
+// VMJavaFrameAnchor layout without a live JVM. Mirrors the same-named
+// accessor already used for this purpose in hotspotMethodId_ut.cpp -- each
+// is a separate translation-unit-local definition; friendship is granted by
+// name+scope, not by a single shared type.
+class VMStructsTestAccessor {
+public:
+ static offset getAnchorPcOffset() { return VMStructs::_anchor_pc_offset; }
+ static void setAnchorPcOffset(offset value) { VMStructs::_anchor_pc_offset = value; }
+};
+
// ---------------------------------------------------------------------------
// A. ProfiledThread thread-type classification (isJavaThread fast path)
//
@@ -576,4 +603,546 @@ TEST_F(SafeFetch64TocTouGuardTest, ZeroReturnMeansGiveUp) {
munmap(page, 4096);
}
+// ---------------------------------------------------------------------------
+// G. HotspotSupport::withUcontextFaultRecovery()'s ucontext restore on a
+// recovered fault -- the crash-protection wrapper walkJavaStack() uses
+// around getJavaTraceAsync()
+//
+// getJavaTraceAsync() mutates the real signal ucontext's pc/sp/fp in place --
+// StackFrame::pc()/sp()/fp() are references straight into uc_mcontext -- while
+// probing AsyncGetCallTrace (e.g. the PROBE_SP retry loop's `frame.sp() +=
+// sizeof(void*)`, or unwindStub()/unwindCompiled() writing pc()/sp()/fp() by
+// reference). It never restores them itself: `ctx_snapshot` is a local of
+// withUcontextFaultRecovery() (hotspotSupport.h), not of getJavaTraceAsync(),
+// and RegisterSnapshot's destructor unconditionally restores pc/sp/fp (and
+// any JavaThread anchor mutation, hotspotStackFrame.h) when that local goes
+// out of scope -- covering every one of getJavaTraceAsync()'s return paths
+// the same way it covers a recovered fault. A SIGSEGV that strikes
+// mid-mutation is caught by checkFault(), which siglongjmp's straight past
+// getJavaTraceAsync() to withUcontextFaultRecovery()'s sigsetjmp; that
+// recovery branch also calls ctx_snapshot.restore() explicitly (ahead of the
+// destructor, since some of its own bookkeeping -- e.g. `*truncated = true`
+// -- must happen before returning too). Since this ucontext is the exact one
+// the kernel uses to resume the sampled thread when the signal handler
+// returns, either way -- normal completion or recovered fault --
+// withUcontextFaultRecovery() must not return with it left mutated.
+//
+// These tests call HotspotSupport::withUcontextFaultRecovery() directly --
+// the exact function walkJavaStack() delegates to -- rather than replicating
+// its sigsetjmp/restore protocol by hand: walkJavaStack() itself can't be
+// invoked here (this gtest binary has no live JVM, and its dispatch asserts
+// VM::isHotspot() / dereferences VMThread state), but the fault-recovery
+// wrapper it calls has no such dependency, so calling it directly means a
+// regression to the real recovery branch (e.g. dropping its ctx_snapshot.
+// restore() call) fails these tests too, instead of only a hand-rolled copy
+// of the same logic.
+//
+// Recovery is driven through the REAL Profiler::checkFault() -- not a
+// simulated siglongjmp -- so its `pc < min || pc >= max` address-range gate
+// (profiler_min_address/_max_address, see stackWalker_ut.cpp's
+// StackWalkerCrashRecoveryTest for the same pattern) is exercised for real.
+// That gate matters here specifically: a fault raised while HotSpot's own
+// AsyncGetCallTrace (libjvm.so) dereferences a poisoned sp/pc/fp has its
+// faulting instruction *inside libjvm.so*, not inside this library, so
+// checkFault() correctly refuses to recover it -- which means
+// withUcontextFaultRecovery()'s sigsetjmp recovery branch (and its own
+// explicit ctx_snapshot.restore() call) is never reached. SetUp() installs a
+// real range via the UNIT_TEST-only Profiler::setAddressRangeForTest() so
+// both sides of that gate -- recovered (pc inside range) and rejected (pc
+// outside range) -- are exercised, rather than only the "always recovers"
+// path.
+//
+// FaultInsideProfilerRangeRecoversAndRestoresUcontext below pins
+// withUcontextFaultRecovery()'s own recovery branch (its sigsetjmp,
+// JmpCtxScope, RegisterSnapshot and set_unwinding_Java restore all run on
+// that path). FaultOutsideProfilerRangeIsNotRecoveredButUcontextIsStillRestored
+// is a negative control for checkFault()'s range gate, not for the recovery
+// branch: on that path checkFault() never siglongjmps, so `work()` just runs
+// to completion and withUcontextFaultRecovery() returns through its normal
+// `work(ctx_snapshot); return partial_result;` path instead. But `ctx_snapshot`
+// still restores the ucontext there too, via its destructor firing on that
+// same return -- so that test's assertions pin RegisterSnapshot's destructor
+// specifically, not the sigsetjmp/JmpCtxScope/checkFault machinery the
+// recovered-fault test above already covers.
+// ---------------------------------------------------------------------------
+
+class WalkJavaStackUcontextRestoreTest : public ::testing::Test {
+protected:
+ // Comfortably covers walkJavaStack's own compiled body in any build
+ // config, while remaining far smaller than the 256MB offset used below
+ // to land clearly outside the range.
+ static constexpr uintptr_t kRangeMargin = 256 * 1024;
+
+ void SetUp() override {
+ ProfiledThread::initCurrentThread();
+ _pt = ProfiledThread::current();
+ ASSERT_NE(nullptr, _pt);
+
+ uintptr_t self_pc = reinterpret_cast(&HotspotSupport::walkJavaStack);
+ _range_lo = self_pc - kRangeMargin;
+ _range_hi = self_pc + kRangeMargin;
+ Profiler::setAddressRangeForTest(_range_lo, _range_hi);
+
+ // Zero-initialized rather than populated via getcontext() -- musl
+ // doesn't provide getcontext(), and this test only round-trips
+ // arbitrary values through StackFrame::pc()/sp()/fp() (references
+ // into uc_mcontext), so a real, live context is unnecessary here.
+ _ctx = ucontext_t{};
+
+ // Seed with distinct non-zero values so the restore assertions below
+ // are actually exercised. A zeroed ucontext makes fp's mutation
+ // (`frame.fp() = saved_sp`, mirroring getJavaTraceAsync) a no-op --
+ // saved_sp is 0, same as fp's own untouched value -- so a broken
+ // restore()/no-restore-at-all would pass EXPECT_EQ(saved_fp,
+ // frame.fp()) by coincidence rather than by actually restoring.
+ StackFrame seed(&_ctx);
+ seed.pc() = 0xAAAA1000;
+ seed.sp() = 0xBBBB2000;
+ seed.fp() = 0xCCCC3000;
+ }
+
+ void TearDown() override {
+ Profiler::resetAddressRangeForTest();
+ ProfiledThread::release();
+ }
+
+ ProfiledThread* _pt = nullptr;
+ ucontext_t _ctx;
+ uintptr_t _range_lo = 0;
+ uintptr_t _range_hi = 0;
+};
+
+// Drives HotspotSupport::withUcontextFaultRecovery() -- the real production
+// function, not a replica -- with `work` mutating the ucontext mid-"walk" the
+// way getJavaTraceAsync does, then taking a fault whose own faulting
+// instruction lands inside this library (e.g. a direct dereference of the
+// poisoned sp, as walkVM's existing INJECT_FAULT_ADDRESS_UNLIKELY sites do)
+// -- so checkFault() must recover. The recovery branch's ctx_snapshot.
+// restore() must undo the mutation -- if that call were ever dropped (the bug
+// this fixes), the EXPECT_EQ calls below would see the corrupted values
+// instead, because they're reading back through the very ucontext
+// withUcontextFaultRecovery() owns.
+TEST_F(WalkJavaStackUcontextRestoreTest, FaultInsideProfilerRangeRecoversAndRestoresUcontext) {
+ StackFrame frame(&_ctx);
+ uintptr_t saved_pc = frame.pc();
+ uintptr_t saved_sp = frame.sp();
+ uintptr_t saved_fp = frame.fp();
+ bool truncated = false;
+ volatile int partial = 0;
+
+ int result = HotspotSupport::withUcontextFaultRecovery(&_ctx, _pt, &truncated, partial, [&](HotspotStackFrame::RegisterSnapshot&) {
+ // Simulate getJavaTraceAsync() mutating the real ucontext mid-walk
+ // (PROBE_SP loop / unwindStub / unwindCompiled all write pc()/sp()/
+ // fp() directly).
+ frame.sp() += sizeof(void*);
+ frame.fp() = saved_sp;
+ frame.pc() = saved_pc + 0x1234;
+
+ // The SIGSEGV's own delivery ucontext -- a distinct object from
+ // _ctx above -- whose faulting pc sits inside the installed range.
+ ucontext_t fault_uc{};
+ StackFrame(&fault_uc).pc() = _range_lo + kRangeMargin;
+
+ siginfo_t si{};
+ si.si_addr = reinterpret_cast(1);
+ // A real SIGSEGV delivery enters this via segvHandler's
+ // SignalHandlerScope before reaching checkFault(); calling checkFault()
+ // directly (no real fault, deterministic pc) means mimicking that
+ // entry ourselves, so SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP()'s
+ // compensating exitSignalScope() -- run inside the recovery branch
+ // below -- has a matching enter to unwind instead of underflowing.
+ _pt->enterSignalScope();
+ Profiler::checkFault(_pt, &si, &fault_uc);
+ ADD_FAILURE() << "unreachable: checkFault() must siglongjmp for an in-range pc";
+ });
+
+ EXPECT_EQ(0, result);
+ EXPECT_TRUE(truncated);
+ EXPECT_EQ(saved_pc, frame.pc());
+ EXPECT_EQ(saved_sp, frame.sp());
+ EXPECT_EQ(saved_fp, frame.fp());
+ EXPECT_FALSE(_pt->isProtected());
+}
+
+// withUcontextFaultRecovery()'s recovery branch returns `partial_result`
+// (hotspotSupport.h), not a hardcoded 0 -- the whole point being that a fault
+// *after* `work` has already committed some progress (e.g. getJavaTraceAsync
+// filling in frames before a later PROBE_SP-loop fault) must surface as a
+// truncated-but-valid count, not get discarded. Every test above leaves
+// `partial` at its initial 0 for the whole call, so a regression that
+// hardcoded `return 0;` in the recovery branch instead of
+// `return partial_result;` would still pass all of them. Set it to a
+// distinguishing non-zero value before faulting to actually pin the
+// read-back this parameter exists for.
+TEST_F(WalkJavaStackUcontextRestoreTest, FaultInsideProfilerRangeRecoversAndPreservesPartialResult) {
+ bool truncated = false;
+ volatile int partial = 0;
+
+ int result = HotspotSupport::withUcontextFaultRecovery(&_ctx, _pt, &truncated, partial, [&](HotspotStackFrame::RegisterSnapshot&) {
+ // Stand in for getJavaTraceAsync() having already committed 3 frames
+ // to the output buffer before a later fault (e.g. inside the
+ // PROBE_SP retry loop) -- the recovery branch must read this back.
+ partial = 3;
+
+ // The SIGSEGV's own delivery ucontext -- a distinct object from
+ // _ctx above -- whose faulting pc sits inside the installed range.
+ ucontext_t fault_uc{};
+ StackFrame(&fault_uc).pc() = _range_lo + kRangeMargin;
+
+ siginfo_t si{};
+ si.si_addr = reinterpret_cast(1);
+ // See the matching comment in FaultInsideProfilerRangeRecoversAndRestoresUcontext.
+ _pt->enterSignalScope();
+ Profiler::checkFault(_pt, &si, &fault_uc);
+ ADD_FAILURE() << "unreachable: checkFault() must siglongjmp for an in-range pc";
+ });
+
+ EXPECT_EQ(3, result)
+ << "a recovered fault must return the partial progress work() already "
+ "committed, not discard it back to 0";
+ EXPECT_TRUE(truncated);
+ EXPECT_FALSE(_pt->isProtected());
+}
+
+// The anchor-restore counterpart of the pc/sp/fp test above: getJavaTraceAsync()'s
+// ticks_unknown_not_Java branch also mutates the real JavaThread's
+// VMJavaFrameAnchor (anchor->setLastJavaPC()) via ctx_snapshot.saveJavaAnchor(),
+// and HotspotStackFrame::RegisterSnapshot::restore() must undo that mutation too
+// on a recovered fault -- the test above never calls saveJavaAnchor(), so it
+// exercises only the base StackFrame::RegisterSnapshot half of restore(), not
+// the derived anchor-restore branch.
+//
+// This gtest binary has no live JVM, so VMStructs::_anchor_pc_offset (and every
+// other vmStructs offset) is never resolved -- it stays at its unresolved
+// default of -1. A fake VMJavaFrameAnchor is built by pointing that offset at 0
+// and reinterpreting a local pointer-sized variable's address as the
+// "anchor": VMJavaFrameAnchor has no data members of its own (only static
+// vmStructs offsets), so its accessors are pure pointer arithmetic over
+// whatever memory they're pointed at -- the same "view over raw memory, never
+// actually constructed" contract every VMStructs-derived type in this codebase
+// relies on (see cast_to()).
+TEST_F(WalkJavaStackUcontextRestoreTest, FaultInsideProfilerRangeRecoversAndRestoresJavaThreadAnchor) {
+ struct RestoreAnchorPcOffset {
+ offset saved = VMStructsTestAccessor::getAnchorPcOffset();
+ ~RestoreAnchorPcOffset() { VMStructsTestAccessor::setAnchorPcOffset(saved); }
+ } restore_anchor_pc_offset;
+ VMStructsTestAccessor::setAnchorPcOffset(0);
+
+ const void* original_pc = reinterpret_cast(0x1111);
+ const void* fake_anchor_storage = original_pc;
+ VMJavaFrameAnchor* anchor = reinterpret_cast(&fake_anchor_storage);
+
+ bool truncated = false;
+ volatile int partial = 0;
+
+ int result = HotspotSupport::withUcontextFaultRecovery(&_ctx, _pt, &truncated, partial, [&](HotspotStackFrame::RegisterSnapshot& ctx_snapshot) {
+ // Mirrors getJavaTraceAsync()'s ticks_unknown_not_Java branch: capture
+ // the anchor's pre-mutation pc via saveJavaAnchor() before patching
+ // it to a new value (hotspotSupport.cpp:1176/1183).
+ ctx_snapshot.saveJavaAnchor(anchor, original_pc);
+ anchor->setLastJavaPC(reinterpret_cast(0xDEAD1234));
+ EXPECT_EQ(reinterpret_cast(0xDEAD1234), anchor->lastJavaPC())
+ << "setLastJavaPC() itself must have taken effect before the fault, "
+ "otherwise the restore assertion below would hold trivially";
+
+ // The SIGSEGV's own delivery ucontext -- a distinct object from _ctx
+ // above -- whose faulting pc sits inside the installed range.
+ ucontext_t fault_uc{};
+ StackFrame(&fault_uc).pc() = _range_lo + kRangeMargin;
+
+ siginfo_t si{};
+ si.si_addr = reinterpret_cast(1);
+ // See the matching comment in FaultInsideProfilerRangeRecoversAndRestoresUcontext.
+ _pt->enterSignalScope();
+ Profiler::checkFault(_pt, &si, &fault_uc);
+ ADD_FAILURE() << "unreachable: checkFault() must siglongjmp for an in-range pc";
+ });
+
+ EXPECT_EQ(0, result);
+ EXPECT_TRUE(truncated);
+ EXPECT_EQ(original_pc, anchor->lastJavaPC())
+ << "a recovered fault must restore the JavaThread anchor's lastJavaPC "
+ "to what it was before saveJavaAnchor() captured it, not leave it "
+ "at the value setLastJavaPC() patched in mid-walk";
+ EXPECT_FALSE(_pt->isProtected());
+}
+
+// A negative control for checkFault()'s address-range gate, not for
+// withUcontextFaultRecovery() itself: a fault whose instruction pointer falls
+// OUTSIDE the profiler's own range -- standing in for a fault raised deep
+// inside libjvm.so while AsyncGetCallTrace dereferences a poisoned sp/pc/fp
+// (see getJavaTraceAsync's anchor-derived fault-injection site) -- must not be
+// recovered by checkFault(). On this path checkFault() never siglongjmps, so
+// `work()` simply runs to completion, and withUcontextFaultRecovery() returns
+// whatever `partial` (below) was last set to through its normal-completion
+// path (hotspotSupport.h's `return partial_result;` after `work(ctx_snapshot)`),
+// not its sigsetjmp recovery branch -- proving the range gate actually
+// distinguishes the two cases rather than always recovering (which is what
+// makes FaultInsideProfilerRangeRecoversAndRestoresUcontext's "recovered"
+// result meaningful).
+//
+// Either way, though, `ctx_snapshot` is a local of withUcontextFaultRecovery
+// itself, and RegisterSnapshot's destructor unconditionally calls restore()
+// on scope exit -- so by the time withUcontextFaultRecovery returns here, the
+// mutation `work()` made has already been undone regardless of which of its
+// two return statements ran. That's a stronger guarantee than "the recovery
+// branch remembers to call restore()": it also covers this unrecovered path,
+// and any future exit from `work()` that forgets to restore explicitly. See
+// the suite-level comment above for the test that exercises the recovery
+// branch itself.
+TEST_F(WalkJavaStackUcontextRestoreTest, FaultOutsideProfilerRangeIsNotRecoveredButUcontextIsStillRestored) {
+ StackFrame frame(&_ctx);
+ uintptr_t saved_pc = frame.pc();
+ uintptr_t saved_sp = frame.sp();
+ uintptr_t saved_fp = frame.fp();
+ bool truncated = false;
+ uintptr_t mutated_pc = 0, mutated_sp = 0, mutated_fp = 0;
+ volatile int partial = 0;
+
+ int result = HotspotSupport::withUcontextFaultRecovery(&_ctx, _pt, &truncated, partial, [&](HotspotStackFrame::RegisterSnapshot&) {
+ // Same mutation getJavaTraceAsync() performs right before handing
+ // sp/pc/fp to jvmAsyncGetCallTrace().
+ frame.sp() += sizeof(void*);
+ frame.fp() = saved_sp;
+ frame.pc() = saved_pc + 0x1234;
+
+ mutated_pc = frame.pc();
+ mutated_sp = frame.sp();
+ mutated_fp = frame.fp();
+
+ // The SIGSEGV's own delivery ucontext, standing in for a fault
+ // inside libjvm.so -- its pc sits 256MB past the installed range,
+ // far beyond kRangeMargin regardless of build config.
+ ucontext_t fault_uc{};
+ StackFrame(&fault_uc).pc() = _range_hi + (256u * 1024 * 1024);
+
+ siginfo_t si{};
+ si.si_addr = reinterpret_cast(1);
+ Profiler::checkFault(_pt, &si, &fault_uc);
+ // Falls through: checkFault must not recover a pc outside the range.
+ partial = 42; // sentinel proving work() ran to completion, unrecovered
+ });
+
+ EXPECT_EQ(42, result) << "checkFault must not have recovered an out-of-range fault";
+ EXPECT_FALSE(truncated);
+ // Sanity: the mutation above actually took effect before ctx_snapshot's
+ // destructor could undo it.
+ EXPECT_NE(saved_pc, mutated_pc);
+ EXPECT_NE(saved_sp, mutated_sp);
+ EXPECT_NE(saved_fp, mutated_fp);
+ EXPECT_EQ(saved_pc, frame.pc())
+ << "ctx_snapshot's destructor restores the ucontext when "
+ "withUcontextFaultRecovery returns, even on this unrecovered path";
+ EXPECT_EQ(saved_sp, frame.sp());
+ EXPECT_EQ(saved_fp, frame.fp());
+ EXPECT_FALSE(_pt->isProtected());
+}
+
+// withUcontextFaultRecovery() must not dereference a null ucontext in its
+// recovery branch, since ucontext can legitimately be null (e.g. malloc/
+// socket hooks sampled outside any signal context).
+TEST_F(WalkJavaStackUcontextRestoreTest, NullUcontextSkipsRestoreWithoutCrashing) {
+ bool truncated = false;
+ volatile int partial = 0;
+
+ int result = HotspotSupport::withUcontextFaultRecovery(nullptr, _pt, &truncated, partial, [&](HotspotStackFrame::RegisterSnapshot&) {
+ // A fault whose pc is inside the installed range, same as the
+ // "recovers" test above, but with a null ucontext -- the recovery
+ // branch's ctx_snapshot.restore() must be a safe no-op here rather
+ // than dereferencing a null StackFrame.
+ ucontext_t fault_uc{};
+ StackFrame(&fault_uc).pc() = _range_lo + kRangeMargin;
+
+ siginfo_t si{};
+ si.si_addr = reinterpret_cast(1);
+ // See the matching comment in FaultInsideProfilerRangeRecoversAndRestoresUcontext.
+ _pt->enterSignalScope();
+ Profiler::checkFault(_pt, &si, &fault_uc);
+ ADD_FAILURE() << "unreachable: checkFault() must siglongjmp for an in-range pc";
+ });
+
+ EXPECT_EQ(0, result);
+ EXPECT_TRUE(truncated);
+ EXPECT_FALSE(_pt->isProtected());
+}
+
#endif // __linux__
+
+#if defined(__APPLE__) && defined(__aarch64__)
+
+// ---------------------------------------------------------------------------
+// H. macOS arm64: JitWriteProtection's W^X (APCTL) restore when its guard
+// outlives the recovered withUcontextFaultRecovery() call
+//
+// asyncJavaTraceWithPostProcessing() (hotspotSupport.cpp) constructs
+// AsyncSampleMutex + JitWriteProtection ABOVE withUcontextFaultRecovery()
+// precisely so their destructors still run when a SIGSEGV is recovered by
+// checkFault()'s siglongjmp: the landing pad lives in
+// withUcontextFaultRecovery()'s own (callee) frame, so the recovered
+// siglongjmp never unwinds the caller's frame, and the guards' destructors
+// fire when that caller returns -- including from the recovery branch.
+// Before that hoist, the guards lived inside work()'s lambda, whose frame
+// the recovered siglongjmp skips -- leaving APCTL write-enabled when the
+// signal handler returns the thread to the JVM, with nothing in this library
+// left to restore it: ~JitWriteProtection (os_macos.cpp) is the restorer.
+//
+// This section pins that layout on the only platform where
+// JitWriteProtection actually does something: on Linux (os_linux.cpp) and
+// macOS x64 the class is inert, so no __linux__-gated test above can catch a
+// regression that moves the guard back inside work(). The register read
+// below (mrs s3_6_c15_c1_5) and the comm-page values mirror
+// JitWriteProtection's own implementation in os_macos.cpp -- there is no
+// public getter for the W^X state. If this ever drifts from os_macos.cpp,
+// os_macos.cpp wins.
+// ---------------------------------------------------------------------------
+
+// The W^X register JitWriteProtection manages -- the same read its
+// constructor uses to snapshot the previous state (os_macos.cpp).
+static u64 readApctl() {
+ u64 v;
+ asm volatile("mrs %0, s3_6_c15_c1_5" : "=r"(v) : :);
+ return v;
+}
+
+class WxRestoreOnRecoveryTest : public ::testing::Test {
+protected:
+ // Same margin as WalkJavaStackUcontextRestoreTest above: comfortably
+ // covers the library's code range in any build config.
+ static constexpr uintptr_t kRangeMargin = 256 * 1024;
+
+ void SetUp() override {
+ ProfiledThread::initCurrentThread();
+ _pt = ProfiledThread::current();
+ ASSERT_NE(nullptr, _pt);
+
+ uintptr_t self_pc = reinterpret_cast(&HotspotSupport::walkJavaStack);
+ _range_lo = self_pc - kRangeMargin;
+ _range_hi = self_pc + kRangeMargin;
+ Profiler::setAddressRangeForTest(_range_lo, _range_hi);
+
+ // Zero-initialized rather than populated via getcontext(), exactly
+ // as in WalkJavaStackUcontextRestoreTest::SetUp -- only pc/sp/fp
+ // round-trip through StackFrame's references here. One macOS-only
+ // difference: uc_mcontext is a *pointer* (Linux embeds the struct),
+ // and StackFrame::pc()/sp()/fp() dereference it
+ // (stackFrame_aarch64.cpp), so _ctx must be backed with real
+ // storage or the RegisterSnapshot capture at the top of
+ // withUcontextFaultRecovery() would itself null-deref.
+ _ctx = ucontext_t{};
+ _ctx.uc_mcontext = &_ctx_mctx;
+ StackFrame seed(&_ctx);
+ seed.pc() = 0xAAAA1000;
+ seed.sp() = 0xBBBB2000;
+ seed.fp() = 0xCCCC3000;
+ }
+
+ void TearDown() override {
+ Profiler::resetAddressRangeForTest();
+ ProfiledThread::release();
+ }
+
+ ProfiledThread* _pt = nullptr;
+ ucontext_t _ctx;
+ _STRUCT_MCONTEXT _ctx_mctx{}; // backing storage for _ctx.uc_mcontext
+ uintptr_t _range_lo = 0;
+ uintptr_t _range_hi = 0;
+};
+
+// Mirrors asyncJavaTraceWithPostProcessing()'s guard layout (the exact
+// production ordering: AsyncSampleMutex, acquired early-return, then
+// JitWriteProtection, all above the withUcontextFaultRecovery() call), and
+// pins that the guards' destructors still fire after a recovered siglongjmp:
+// the enclosing scope's exit must leave APCTL exactly as it was before the
+// walk. If the guard were moved back inside work()'s lambda (the original
+// bug layout), the recovered siglongjmp would skip ~JitWriteProtection and
+// the final EXPECT_EQ(before, after) below would fail with APCTL still
+// write-enabled when the signal handler returns.
+TEST_F(WxRestoreOnRecoveryTest, GuardsOutsideRecoveryRegionRestoreApctlAfterRecoveredFault) {
+ // JitWriteProtection's own support check (os_macos.cpp): without APRR
+ // the class is inert on this system and the W^X hazard cannot exist.
+ if (!*(volatile char*)0xfffffc10c) {
+ GTEST_SKIP() << "no APRR support on this system: JitWriteProtection is inert";
+ }
+ // The comm-page register values JitWriteProtection itself writes for
+ // enable(true) / enable(false) (os_macos.cpp).
+ const u64 protected_val = *(volatile u64*)0xfffffc118;
+ const u64 unprotected_val = *(volatile u64*)0xfffffc110;
+ ASSERT_NE(protected_val, unprotected_val)
+ << "comm page must provide distinct APCTL values for protected/unprotected";
+
+ StackFrame frame(&_ctx);
+ uintptr_t saved_pc = frame.pc();
+ uintptr_t saved_sp = frame.sp();
+ uintptr_t saved_fp = frame.fp();
+
+ const u64 before = readApctl();
+ ASSERT_EQ(before, protected_val)
+ << "precondition: a non-JIT process must start write-protected";
+
+ u64 after = 0;
+ bool truncated = false;
+ volatile int partial = 0;
+
+ {
+ // Production ordering from asyncJavaTraceWithPostProcessing()
+ // (hotspotSupport.cpp): the mutex gates re-entrant walks, the JIT
+ // guard flips W^X for the AGCT path, and both outlive the wrapper.
+ AsyncSampleMutex mutex(_pt);
+ ASSERT_TRUE(mutex.acquired());
+ JitWriteProtection jit(false);
+
+ u64 under_guard = 0;
+ int result = HotspotSupport::withUcontextFaultRecovery(
+ &_ctx, _pt, &truncated, partial,
+ [&](HotspotStackFrame::RegisterSnapshot&) {
+ // While the guard is live, APCTL must read back the
+ // write-allowed value -- pinning non-vacuity: if
+ // JitWriteProtection failed to toggle here, the restore
+ // assertion below would pass for the wrong reason.
+ under_guard = readApctl();
+
+ // Fault with a delivery pc inside the installed range, the
+ // same pattern as FaultInsideProfilerRangeRecoversAndRestores
+ // Ucontext above: enterSignalScope() pairs with the recovery
+ // branch's SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP()
+ // compensation (see the matching comment there). The
+ // delivery ucontext is backed with real mcontext storage for
+ // the same reason as _ctx in SetUp above.
+ ucontext_t fault_uc{};
+ _STRUCT_MCONTEXT fault_mctx{};
+ fault_uc.uc_mcontext = &fault_mctx;
+ StackFrame(&fault_uc).pc() = _range_lo + kRangeMargin;
+
+ siginfo_t si{};
+ si.si_addr = reinterpret_cast(1);
+ _pt->enterSignalScope();
+ Profiler::checkFault(_pt, &si, &fault_uc);
+ ADD_FAILURE() << "unreachable: checkFault() must siglongjmp for an in-range pc";
+ });
+
+ EXPECT_EQ(0, result);
+ EXPECT_TRUE(truncated);
+ EXPECT_EQ(unprotected_val, under_guard)
+ << "non-vacuity: JitWriteProtection must have toggled APCTL inside work()";
+ EXPECT_EQ(unprotected_val, readApctl())
+ << "the recovery branch itself must not have restored APCTL -- the "
+ "restore belongs to the guard's destructor at scope exit";
+ } // ~JitWriteProtection + ~AsyncSampleMutex run here, as they would at
+ // asyncJavaTraceWithPostProcessing()'s return on the recovery path.
+
+ after = readApctl();
+ EXPECT_FALSE(_pt->is_unwinding_Java())
+ << "AsyncSampleMutex must clear the per-thread guard at scope exit";
+
+ EXPECT_EQ(before, after)
+ << "W^X (APCTL) state must be restored when the guards' scope exits "
+ "after a recovered fault";
+
+ // The recovery branch ran and restored the ucontext as in section G.
+ EXPECT_EQ(saved_pc, frame.pc());
+ EXPECT_EQ(saved_sp, frame.sp());
+ EXPECT_EQ(saved_fp, frame.fp());
+ EXPECT_FALSE(_pt->isProtected());
+}
+
+#endif // __APPLE__ && __aarch64__
diff --git a/ddprof-lib/src/test/cpp/safefetch_ut.cpp b/ddprof-lib/src/test/cpp/safefetch_ut.cpp
index 769f891e9f..472502ee3e 100644
--- a/ddprof-lib/src/test/cpp/safefetch_ut.cpp
+++ b/ddprof-lib/src/test/cpp/safefetch_ut.cpp
@@ -112,6 +112,97 @@ TEST_F(SafeFetchTest, invalidAccessPtr) {
EXPECT_EQ(res, bp);
}
+// ---------------------------------------------------------------------------
+// SafeAccess::store/store32/storePtr — write-side counterpart of
+// safeFetch{32,64}/loadPtr, sharing the same handle_safefetch fault redirect.
+// ---------------------------------------------------------------------------
+
+TEST_F(SafeFetchTest, validStore32) {
+ int32_t i = 0;
+ EXPECT_TRUE(SafeAccess::store32(&i, 42));
+ EXPECT_EQ(42, i);
+ EXPECT_TRUE(SafeAccess::store32(&i, INT_MIN));
+ EXPECT_EQ(INT_MIN, i);
+}
+
+TEST_F(SafeFetchTest, invalidStore32) {
+ int32_t* p = nullptr;
+ EXPECT_FALSE(SafeAccess::store32(p, 42));
+}
+
+TEST_F(SafeFetchTest, validStorePtr) {
+ void* target = nullptr;
+ void** pp = ⌖
+ char c;
+ EXPECT_TRUE(SafeAccess::storePtr(pp, (void*)&c));
+ EXPECT_EQ((void*)&c, target);
+}
+
+TEST_F(SafeFetchTest, invalidStorePtr) {
+ void** pp = nullptr;
+ char c;
+ EXPECT_FALSE(SafeAccess::storePtr(pp, (void*)&c));
+}
+
+TEST_F(SafeFetchTest, validStore) {
+ void* target = nullptr;
+ void** pp = ⌖
+ char c;
+ EXPECT_TRUE(SafeAccess::store(pp, (void*)&c));
+ EXPECT_EQ((void*)&c, target);
+}
+
+TEST_F(SafeFetchTest, invalidStore) {
+ void** pp = nullptr;
+ char c;
+ EXPECT_FALSE(SafeAccess::store(pp, (void*)&c));
+}
+
+/**
+ * Tests that store32 correctly handles a read-only page instead of crashing.
+ * PROT_READ (rather than PROT_NONE) isolates that this is specifically a
+ * write fault, not merely an unreadable address.
+ */
+TEST_F(SafeFetchTest, readOnlyMemoryStore32) {
+ void* page = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(page, MAP_FAILED);
+
+ int32_t* ptr = static_cast(page);
+ *ptr = 1;
+ ASSERT_EQ(mprotect(page, 4096, PROT_READ), 0);
+
+ // This MUST return false, not crash.
+ EXPECT_FALSE(SafeAccess::store32(ptr, 2));
+ // The page is read-only, so the value must be unchanged.
+ ASSERT_EQ(mprotect(page, 4096, PROT_READ | PROT_WRITE), 0);
+ EXPECT_EQ(1, *ptr);
+
+ munmap(page, 4096);
+}
+
+/**
+ * Tests that storePtr correctly handles a read-only page instead of crashing.
+ * Same rationale as readOnlyMemoryStore32 above.
+ */
+TEST_F(SafeFetchTest, readOnlyMemoryStorePtr) {
+ void* page = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(page, MAP_FAILED);
+
+ void** ptr = static_cast(page);
+ char sentinel;
+ *ptr = &sentinel;
+ ASSERT_EQ(mprotect(page, 4096, PROT_READ), 0);
+
+ char other;
+ EXPECT_FALSE(SafeAccess::storePtr(ptr, &other));
+ ASSERT_EQ(mprotect(page, 4096, PROT_READ | PROT_WRITE), 0);
+ EXPECT_EQ((void*)&sentinel, *ptr);
+
+ munmap(page, 4096);
+}
+
TEST_F(SafeFetchTest, isReadable) {
char c = 'x';
EXPECT_TRUE(SafeAccess::isReadable(&c));