diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index 109fd415b3..8c2c877cc1 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -15,6 +15,7 @@ #include "hotspot/vmStructs.inline.h" #include "jvmSupport.inline.h" #include "jvmThread.h" +#include "log.h" #include "profiler.h" #include "dwarfStep.inline.h" #include "stackWalker.inline.h" @@ -1352,25 +1353,132 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { return 0; } -static void patchClassLoaderData(JNIEnv* jni, jclass klass) { +class LockState { +private: + VMClassLoaderData* volatile _cld; +public: + // Non-copyable + LockState(const LockState&) = delete; + LockState& operator=(const LockState&) = delete; + + inline LockState() : _cld(nullptr) {} + inline ~LockState() { reset(); } + inline void lock(VMClassLoaderData* cld); + inline void reset(); +}; + +void LockState::lock(VMClassLoaderData* cld) { + // This call can fault inside JVM, nothing we can do about it + cld->lock(); + // The store must immediately follow the acquire + _cld = cld; +} + +void LockState::reset() { + // Assume: if _cld->lock() did not fail, _cld->unlock() should not + // fail neither. + // The siglongjmp cannot protect _cld->lock()/unlock() calls + if (_cld != nullptr) { + _cld->unlock(); + _cld = nullptr; + } +} + +// Returns the tag value that should be persisted on `klass` once its +// jmethodIDs have actually been loaded, or -1 if no tag update is needed +// (not JDK 8, no capacity gap to fill, or the patch itself could not run). +// The caller must not call SetTag() with this value until it has confirmed +// the freshly-prepended capacity was put to use -- see loadMethodIDsIfNeededImpl(). +// +// force_patch must be true for RedefineClasses/RetransformClasses callers. +// Those invalidate this class's existing jmethodIDs even when method_count +// is unchanged, so GetClassMethods() below is about to allocate brand new +// jmethodID slots regardless of what the persisted tag says. Trusting the +// tag there would skip prepending fresh MethodList capacity, and the CLD-wide +// free list an earlier patch left behind may already have been drained by +// other classes loaded in the meantime -- sending this reallocation onto +// HotSpot's slow list path that this workaround exists to avoid +// (JDK-8062116). force_patch bypasses the tag and re-patches from 0 every +// time; the cost is leaving the old, now-orphaned blocks in place, the same +// jmethodID growth under class churn already accepted elsewhere (see +// JVMSupport::loadMethodIDsImpl()). +static jlong patchClassLoaderData(JNIEnv* jni, jclass klass, bool force_patch) { bool needs_patch = VM::hotspot_version() == 8; - if (needs_patch) { - // Workaround for JVM bug https://bugs.openjdk.org/browse/JDK-8062116 - // Preallocate space for jmethodIDs at the beginning of the list (rather than at the end) - // This is relevant only for JDK 8 - later versions do not have this bug - if (VMStructs::hasClassLoaderData()) { - VMKlass *vmklass = VMKlass::fromJavaClass(jni, klass); - int method_count = vmklass->methodCount(); - if (method_count > 0) { - VMClassLoaderData *cld = vmklass->classLoaderData(); - cld->lock(); - for (int i = 0; i < method_count; i += MethodList::SIZE) { - *cld->methodList() = new MethodList(*cld->methodList()); - } - cld->unlock(); - } + if (!needs_patch || !VMStructs::hasClassLoaderData()) { + return -1; + } + // Workaround for JVM bug https://bugs.openjdk.org/browse/JDK-8062116 + // Preallocate space for jmethodIDs at the beginning of the list (rather than at the end) + // This is relevant only for JDK 8 - later versions do not have this bug + ProfiledThread* prof_thread = ProfiledThread::initCurrentThreadSignalSafe(); + // Not exercised by hotspotSupport_ut.cpp: initCurrentThreadSignalSafe() + // only returns null when isThreadKeyValid() is false, i.e. the process-wide + // pthread_key_create() in ProfiledThread's static initializer failed at + // library load. That key is shared by every ProfiledThread lookup in the + // binary, so there is no per-test seam to flip it false without also + // breaking TLS for every other test running in the same gtest binary. + if (prof_thread == nullptr) { + return -1; + } + JmpCtxScope jmp_scope(prof_thread); + sigjmp_buf crash_protection_ctx; + LockState state; + if (sigsetjmp(crash_protection_ctx, 1) != 0) { + SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP(); + jmp_scope.restore(); + state.reset(); + return -1; + } + jmp_scope.install(&crash_protection_ctx); + VMKlass *vmklass = VMKlass::fromJavaClass(jni, klass); + if (vmklass == nullptr) { + return -1; + } + int method_count = vmklass->methodCount(); + if (method_count <= 0) { + return -1; + } + // patchClassLoaderData() re-runs for the same class on every ClassPrepare + // replay (profiler restart via loadAllMethodIDsIfNeeded()) -- which does not + // change method_count and does not invalidate existing jmethodIDs, so + // topping up only the tail [already_patched, method_count) (or skipping + // entirely when method_count hasn't grown) is safe. Without this tag, each + // replay would prepend another full set of MethodList blocks onto the + // classloader-wide list that nothing ever frees. The tag lives on the + // jclass itself, so it disappears with the class -- no separate bookkeeping + // to leak or to clean up on unload. + // RedefineClasses/RetransformClasses callers pass force_patch=true instead + // of relying on this tag -- see the function comment above. + jlong already_patched = 0; + if (!force_patch) { + jvmtiEnv* jvmti = VM::jvmti(); + if (jvmti == nullptr || jvmti->GetTag(klass, &already_patched) != JVMTI_ERROR_NONE) { + already_patched = 0; + } + if (method_count <= already_patched) { + return -1; } } + VMClassLoaderData *cld = vmklass->classLoaderData(); + if (cld == nullptr) { + return -1; + } + state.lock(cld); + // Inject fault to test _cld->unlock() + INJECT_CRASH_LIKELY(); + + jlong i; + for (i = already_patched; i < method_count; i += MethodList::SIZE) { + *cld->methodList() = new MethodList(*cld->methodList()); + } + // Release cld's lock before returning to the caller, which may go on to + // call JVMTI entry points (GetClassMethods, SetTag): cld->lock() resolves + // to HotSpot's Monitor::lock_without_safepoint_check(), and those JVMTI + // calls do participate in safepoint polling -- calling them while holding + // a safepoint-check-suppressing lock is a JVM-deadlock hazard on its own, + // independent of any fault/siglongjmp path. + state.reset(); + return i; } constexpr const char LAMBDA_PREFIX[] = "Ljava/lang/invoke/LambdaForm$"; @@ -1422,7 +1530,7 @@ static bool isHiddenClassBySignature(const char* signature) { return slash != nullptr && slash[1] >= '0' && slash[1] <= '9'; } -bool HotspotSupport::loadMethodIDsIfNeededImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass, bool load_all) { +bool HotspotSupport::loadMethodIDsIfNeededImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass, bool load_all, bool force_patch) { if (!load_all) { jobject cl = nullptr; // Hidden/lambda classes can be unloaded, fallback to use jmethodIDs, so preload them. @@ -1449,8 +1557,37 @@ bool HotspotSupport::loadMethodIDsIfNeededImpl(jvmtiEnv *jvmti, JNIEnv *jni, jcl jni->DeleteLocalRef(cl); } } - patchClassLoaderData(jni, klass); - return JVMSupport::loadMethodIDsImpl(jvmti, jni, klass); + jlong new_tag = patchClassLoaderData(jni, klass, force_patch); + bool loaded = JVMSupport::loadMethodIDsImpl(jvmti, jni, klass); + // Only persist the tag once loadMethodIDsImpl() has actually run against + // the freshly-prepended capacity. Setting it unconditionally (regardless + // of whether GetClassMethods below succeeds) would permanently block + // future top-ups for a class whose own jmethodIDs were never allocated -- + // e.g. if GetClassMethods fails here after other classes in the same CLD + // consume the slots just prepended, a later successful retry would find + // the shared free list empty with no way to top it up again, since + // patchClassLoaderData() would see method_count <= already_patched. + // Use VM::jvmti(), not the jvmti argument above -- patchClassLoaderData() + // reads VM::jvmti() for GetTag, so SetTag must come from the same env for + // the two to agree on the tag's meaning. + if (loaded && new_tag >= 0) { + jvmtiEnv* patch_jvmti = VM::jvmti(); + if (patch_jvmti != nullptr) { + jvmtiError tag_err = patch_jvmti->SetTag(klass, new_tag); + // A lost SetTag has no correctness impact (loaded is still returned + // as computed above), but it silently degrades patchClassLoaderData()'s + // tag-based dedup back to re-prepending the same MethodList blocks on + // every future replay of this class -- the unbounded growth the + // workaround exists to prevent. Log it so that degradation is + // diagnosable instead of invisible. + if (tag_err != JVMTI_ERROR_NONE) { + Log::warn("patchClassLoaderData: SetTag failed (jvmtiError=%d); " + "this class's MethodList capacity will be re-patched on the next ClassPrepare replay", + tag_err); + } + } + } + return loaded; } // The three names resolve() needs, owned by resolve()'s frame. Each name has @@ -1687,7 +1824,7 @@ static bool readMethodNames(const void* method, VMMethod** out_vm_method, // release call is needed), or nullptr if the method could not be found via // JNI/JVMTI. static jmethodID lookupMethodIdViaJni(VMMethod* vm_method, const ResolvedNames& names, - bool (*loadMethodIDsIfNeededImpl)(jvmtiEnv*, JNIEnv*, jclass, bool)) { + bool (*loadMethodIDsIfNeededImpl)(jvmtiEnv*, JNIEnv*, jclass, bool, bool)) { jmethodID method_id = nullptr; const char* method_name = names.methodName(); const char* method_signature = names.methodSignature(); @@ -1715,7 +1852,7 @@ static jmethodID lookupMethodIdViaJni(VMMethod* vm_method, const ResolvedNames& if (strcmp(method_name, "") == 0) { jvmtiEnv* jvmti = VM::jvmti(); if (jvmti != nullptr) { - if (loadMethodIDsIfNeededImpl(jvmti, jni, clz, true /*load all*/)) { + if (loadMethodIDsIfNeededImpl(jvmti, jni, clz, true /*load all*/, false /*force_patch*/)) { jmethodID validated = vm_method->validatedId(); if (isValidJMethodID(validated)) { method_id = validated; diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h index 8588635dfc..018dcfc2f6 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h @@ -47,7 +47,7 @@ class HotspotSupport { int max_depth, StackContext *java_ctx, bool *truncated, HotspotStackFrame::RegisterSnapshot& ctx_snapshot); - static bool loadMethodIDsIfNeededImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass, bool load_all); + static bool loadMethodIDsIfNeededImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass, bool load_all, bool force_patch); // Runs getJavaTraceAsync() under withUcontextFaultRecovery(), then layers // on its two post-processing steps: resolving frame types for the top diff --git a/ddprof-lib/src/main/cpp/hotspot/vmStructs.h b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h index c61ccbc3b3..9ae8868e4e 100644 --- a/ddprof-lib/src/main/cpp/hotspot/vmStructs.h +++ b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h @@ -606,6 +606,8 @@ class MethodList { _method[i] = 0x37; } } + + friend class MethodListTestAccessor; }; diff --git a/ddprof-lib/src/main/cpp/jvmSupport.cpp b/ddprof-lib/src/main/cpp/jvmSupport.cpp index 4dc2be966e..26300929fd 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.cpp +++ b/ddprof-lib/src/main/cpp/jvmSupport.cpp @@ -143,7 +143,7 @@ void JVMSupport::loadAllMethodIDsIfNeeded(jvmtiEnv *jvmti, JNIEnv *jni) { if (jvmti->GetLoadedClasses(&class_count, &classes) == JVMTI_ERROR_NONE) { for (int i = 0; i < class_count; i++) { - if(loadMethodIDsIfNeeded(jvmti, jni, classes[i])) { + if(loadMethodIDsIfNeeded(jvmti, jni, classes[i], /*force_patch=*/false)) { loaded_count++; } } @@ -152,7 +152,7 @@ void JVMSupport::loadAllMethodIDsIfNeeded(jvmtiEnv *jvmti, JNIEnv *jni) { TEST_LOG("Preloaded jmethodIDs for %d/%d classes", loaded_count, class_count); } -bool JVMSupport::loadMethodIDsIfNeeded(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass) { +bool JVMSupport::loadMethodIDsIfNeeded(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass, bool force_patch) { JMethodIDLoadStats state = getLoadState(); // Callback from JVMTI for class loading - We don't have to deal with it before // the first execution - loadAllMethodIDsIfNeeded() will fix it. @@ -161,7 +161,7 @@ bool JVMSupport::loadMethodIDsIfNeeded(jvmtiEnv *jvmti, JNIEnv *jni, jclass klas } if (VM::isHotspot()) { - return HotspotSupport::loadMethodIDsIfNeededImpl(jvmti, jni, klass, state == Fully_loaded /* load all */); + return HotspotSupport::loadMethodIDsIfNeededImpl(jvmti, jni, klass, state == Fully_loaded /* load all */, force_patch); } else { return loadMethodIDsImpl(jvmti, jni, klass); } diff --git a/ddprof-lib/src/main/cpp/jvmSupport.h b/ddprof-lib/src/main/cpp/jvmSupport.h index 49b8a9193f..a3302159ae 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.h +++ b/ddprof-lib/src/main/cpp/jvmSupport.h @@ -64,7 +64,7 @@ class JVMSupport { static inline long long runtimeStubsMemoryUsage(); static void loadAllMethodIDsIfNeeded(jvmtiEnv *jvmti, JNIEnv *jni); - static bool loadMethodIDsIfNeeded(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass); + static bool loadMethodIDsIfNeeded(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass, bool force_patch); // Resolve method pointer to jmethodID static inline jmethodID resolve(const void* method); diff --git a/ddprof-lib/src/main/cpp/vmEntry.cpp b/ddprof-lib/src/main/cpp/vmEntry.cpp index 17bde30d9f..baf57173a6 100644 --- a/ddprof-lib/src/main/cpp/vmEntry.cpp +++ b/ddprof-lib/src/main/cpp/vmEntry.cpp @@ -628,7 +628,7 @@ void *VM::getLibraryHandle(const char *name) { void JNICALL VM::ClassPrepare(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jclass klass) { ProfiledThread::initCurrentThreadSignalSafe(); - JVMSupport::loadMethodIDsIfNeeded(jvmti, jni, klass); + JVMSupport::loadMethodIDsIfNeeded(jvmti, jni, klass, /*force_patch=*/false); } void JNICALL VM::ClassLoad(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread, @@ -669,11 +669,12 @@ VM::RedefineClassesHook(jvmtiEnv *jvmti, jint class_count, _orig_RedefineClasses(jvmti, class_count, class_definitions); if (result == 0) { - // jmethodIDs are invalidated after RedefineClasses + // jmethodIDs are invalidated after RedefineClasses -- force_patch=true; + // see patchClassLoaderData() for why the persisted tag can't be trusted here. JNIEnv *env = jni(); for (int i = 0; i < class_count; i++) { if (class_definitions[i].klass != NULL) { - JVMSupport::loadMethodIDsIfNeeded(jvmti, env, class_definitions[i].klass); + JVMSupport::loadMethodIDsIfNeeded(jvmti, env, class_definitions[i].klass, /*force_patch=*/true); } } } @@ -686,11 +687,12 @@ jvmtiError VM::RetransformClassesHook(jvmtiEnv *jvmti, jint class_count, jvmtiError result = _orig_RetransformClasses(jvmti, class_count, classes); if (result == 0) { - // jmethodIDs are invalidated after RetransformClasses + // Same reasoning as RedefineClassesHook above: jmethodIDs are invalidated + // after RetransformClasses too, so force_patch=true. JNIEnv *env = jni(); for (int i = 0; i < class_count; i++) { if (classes[i] != NULL) { - JVMSupport::loadMethodIDsIfNeeded(jvmti, env, classes[i]); + JVMSupport::loadMethodIDsIfNeeded(jvmti, env, classes[i], /*force_patch=*/true); } } } diff --git a/ddprof-lib/src/test/cpp/hotspotSupport_ut.cpp b/ddprof-lib/src/test/cpp/hotspotSupport_ut.cpp index 42ba2d7176..58d60d461b 100644 --- a/ddprof-lib/src/test/cpp/hotspotSupport_ut.cpp +++ b/ddprof-lib/src/test/cpp/hotspotSupport_ut.cpp @@ -4,7 +4,20 @@ #include #include "../../main/cpp/hotspot/hotspotSupport.h" +#include "../../main/cpp/hotspot/vmStructs.h" #include "../../main/cpp/gtest_crash_handler.h" +#include "../../main/cpp/threadLocalData.h" +#include "../../main/cpp/vmEntry.h" + +#include +#include + +#ifdef __FAULT_INJECTION__ +#include "../../main/cpp/guards.h" +#include "../../main/cpp/os.h" +#include "../../main/cpp/profiler.h" +#include "../../main/cpp/threadLocalData.inline.h" +#endif static constexpr char HOTSPOT_SUPPORT_TEST_NAME[] = "HotspotSupportTest"; class HotspotSupportGlobalSetup { @@ -22,8 +35,8 @@ static HotspotSupportGlobalSetup hotspot_support_global_setup; // --------------------------------------------------------------------------- class HotspotSupportTestAccessor { public: - static bool loadMethodIDsIfNeededImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass, bool load_all) { - return HotspotSupport::loadMethodIDsIfNeededImpl(jvmti, jni, klass, load_all); + static bool loadMethodIDsIfNeededImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass, bool load_all, bool force_patch) { + return HotspotSupport::loadMethodIDsIfNeededImpl(jvmti, jni, klass, load_all, force_patch); } }; @@ -65,7 +78,7 @@ TEST_F(HotspotSupportLoadMethodIDsTest, LoadAllSucceedsAndCallsGetClassMethodsOn jclass fake_klass = reinterpret_cast(0x1); bool result = HotspotSupportTestAccessor::loadMethodIDsIfNeededImpl( - &mock_jvmti, /*jni=*/nullptr, fake_klass, /*load_all=*/true); + &mock_jvmti, /*jni=*/nullptr, fake_klass, /*load_all=*/true, /*force_patch=*/false); EXPECT_TRUE(result); EXPECT_EQ(1, g_get_class_methods_calls) @@ -73,3 +86,645 @@ TEST_F(HotspotSupportLoadMethodIDsTest, LoadAllSucceedsAndCallsGetClassMethodsOn "(load_all=true), which is what now applies patchClassLoaderData() " "before allocating jmethodIDs"; } + +// --------------------------------------------------------------------------- +// patchClassLoaderData() tag/resume regression tests +// +// patchClassLoaderData() (the JDK-8062116 preallocation workaround, only +// active on JDK 8) tags each jclass with the MethodList capacity boundary +// its last patch reached -- the loop's exit value, i.e. the next block +// boundary at or past the method count that was patched, not that method +// count itself (see SecondCallPatchesOnlyNewMethodRange below, where a +// method_count of 20 tags 24). This lets a replayed ClassPrepare +// (loadAllMethodIDsIfNeeded() on profiler restart) patch only the new tail +// [already_patched, method_count) -- or skip entirely when method_count +// hasn't grown -- instead of re-prepending a full set of blocks every time. +// +// RedefineClasses/RetransformClasses do not get this tag-based skip +// (force_patch=true): they invalidate existing jmethodIDs even when +// method_count is unchanged, so the tag alone cannot tell +// patchClassLoaderData() whether fresh capacity is still needed. +// +// The test above never exercises this: it leaves VM::hotspot_version() at +// its gtest-binary default (not 8), so patchClassLoaderData() is a no-op +// there. These tests force hotspot_version 8 and fake the +// VMKlass/VMClassLoaderData memory layout so the tag/resume loop itself +// runs. +// --------------------------------------------------------------------------- + +// Friend of VM: lets these tests force isHotspot()/hotspot_version()==8 and +// swap VM::jvmti() for a mock. patchClassLoaderData() reads VM::jvmti() +// directly -- not the jvmti argument threaded through +// loadMethodIDsIfNeededImpl -- for GetTag/SetTag, so both must point at the +// same mock. +class VMTestAccessor { +public: + static bool getHotspot() { return VM::_hotspot; } + static void setHotspot(bool v) { VM::_hotspot = v; } + static int getHotspotVersion() { return VM::_hotspot_version; } + static void setHotspotVersion(int v) { VM::_hotspot_version = v; } + static jvmtiEnv* getJvmti() { return VM::_jvmti; } + static void setJvmti(jvmtiEnv* env) { VM::_jvmti = env; } +}; + +// Friend of VMStructs: lets these tests point VMKlass::methodCount()/ +// classLoaderData() and VMClassLoaderData::lock()/unlock()/methodList() at a +// fake in-memory layout instead of real HotSpot metadata. +class VMStructsTestAccessor { +public: + struct State { + bool has_class_loader_data; + int methods_offset; + int class_loader_data_offset; + uint64_t vmklass_size; + uint64_t vmcld_size; + VMStructs::LockFunc lock_func; + VMStructs::LockFunc unlock_func; + }; + + static State save() { + return State{ + VMStructs::_has_class_loader_data, + VMStructs::_methods_offset, + VMStructs::_class_loader_data_offset, + VMStructs::TYPE_SIZE_NAME(VMKlass), + VMStructs::TYPE_SIZE_NAME(VMClassLoaderData), + VMStructs::_lock_func, + VMStructs::_unlock_func, + }; + } + + static void apply(const State& s) { + VMStructs::_has_class_loader_data = s.has_class_loader_data; + VMStructs::_methods_offset = s.methods_offset; + VMStructs::_class_loader_data_offset = s.class_loader_data_offset; + VMStructs::TYPE_SIZE_NAME(VMKlass) = s.vmklass_size; + VMStructs::TYPE_SIZE_NAME(VMClassLoaderData) = s.vmcld_size; + VMStructs::_lock_func = s.lock_func; + VMStructs::_unlock_func = s.unlock_func; + } +}; + +// Friend of MethodList: patchClassLoaderData() prepends nodes to a private +// linked list with no production accessor. Walking _next is the only way to +// count how many nodes a given call actually prepended. +class MethodListTestAccessor { +public: + static const MethodList* next(const MethodList* node) { return node->_next; } +}; + +static int methodListChainLength(const MethodList* head) { + int n = 0; + while (head != nullptr) { + n++; + head = MethodListTestAccessor::next(head); + } + return n; +} + +namespace { + +// Fake ClassLoaderData. VMClassLoaderData::mutex() and methodList() index at +// fixed byte offsets from `this` (sizeof(uintptr_t)*3 and *6+8 respectively) +// baked into vmStructs.h -- not offsets this test controls -- so the fake +// layout must match those exactly. +struct FakePatchCLD { + alignas(sizeof(void*)) char pad0[sizeof(uintptr_t) * 3]; + void* mutex_ptr; // consumed by mutex(); never dereferenced by the no-op lock/unlock mocks below. + char pad1[sizeof(uintptr_t) * 6 + 8 - (sizeof(uintptr_t) * 3 + sizeof(void*))]; + MethodList* method_list_head; +}; +static_assert(offsetof(FakePatchCLD, mutex_ptr) == sizeof(uintptr_t) * 3, + "mutex() offset drifted"); +static_assert(offsetof(FakePatchCLD, method_list_head) == sizeof(uintptr_t) * 6 + 8, + "methodList() offset drifted"); + +// Fake VMKlass: a methods-table pointer (methodCount() masks its low 16 +// bits) and a ClassLoaderData pointer, at offsets this test wires up via +// VMStructsTestAccessor. +struct FakePatchKlass { + int* methods_table; + FakePatchCLD* cld; +}; + +int g_lock_calls = 0; +int g_unlock_calls = 0; +void noopLock(void*) { g_lock_calls++; } +void noopUnlock(void*) { g_unlock_calls++; } + +jlong g_tag = 0; +int g_get_tag_calls = 0; +int g_set_tag_calls = 0; +jlong g_last_set_tag_value = -1; + +jvmtiError JNICALL mock_GetTag(jvmtiEnv*, jobject, jlong* tag_ptr) { + g_get_tag_calls++; + *tag_ptr = g_tag; + return JVMTI_ERROR_NONE; +} +jvmtiError JNICALL mock_SetTag(jvmtiEnv*, jobject, jlong tag) { + g_set_tag_calls++; + g_tag = tag; + g_last_set_tag_value = tag; + return JVMTI_ERROR_NONE; +} + +// Simulates a JVMTI implementation that fails GetTag but still writes through +// *tag_ptr (undefined by the spec, but not something callers may rely on +// being left untouched). The garbage value looks like "already fully +// patched" if a caller trusted it instead of falling back to 0 on error. +jvmtiError JNICALL mock_GetTag_garbage_on_error(jvmtiEnv*, jobject, jlong* tag_ptr) { + g_get_tag_calls++; + *tag_ptr = 999999; + return JVMTI_ERROR_INVALID_OBJECT; +} + +// Same shape as mock_GetTag_garbage_on_error, but with a different failure +// code -- pins the check to "any GetTag failure", not to +// JVMTI_ERROR_INVALID_OBJECT specifically. Without this second code, a +// mutant narrowing `!= JVMTI_ERROR_NONE` to `== JVMTI_ERROR_INVALID_OBJECT` +// would still pass GetTagFailureIgnoresGarbageAndRestartsFromZero (both +// sides agree there) but survive undetected. +jvmtiError JNICALL mock_GetTag_wrong_phase_with_garbage(jvmtiEnv*, jobject, jlong* tag_ptr) { + g_get_tag_calls++; + *tag_ptr = 999999; + return JVMTI_ERROR_WRONG_PHASE; +} + +// Simulates SetTag failing after a successful patch (e.g. JVMTI_ERROR_INVALID_CLASS +// during unload, or an env/capability mismatch) -- deliberately does not update +// g_tag, matching a real JVMTI implementation that leaves the tag untouched on +// failure. +jvmtiError JNICALL mock_SetTag_fails(jvmtiEnv*, jobject, jlong tag) { + g_set_tag_calls++; + g_last_set_tag_value = tag; + return JVMTI_ERROR_INVALID_CLASS; +} + +FakePatchKlass* g_fake_klass_for_jni = nullptr; + +jlong JNICALL mock_GetLongField(JNIEnv*, jobject, jfieldID) { + return (jlong)(intptr_t)g_fake_klass_for_jni; +} + +} // namespace + +class PatchClassLoaderDataTest : public ::testing::Test { +protected: + jvmtiInterface_1_ tbl{}; + _jvmtiEnv mock_jvmti{}; + JNINativeInterface_ jni_tbl{}; + JNIEnv_ mock_jni{}; + + FakePatchCLD fake_cld{}; + FakePatchKlass fake_klass{}; + int methods_header = 0; + + bool saved_hotspot = false; + int saved_hotspot_version = 0; + jvmtiEnv* saved_jvmti = nullptr; + VMStructsTestAccessor::State saved_structs{}; + + void SetUp() override { + saved_hotspot = VMTestAccessor::getHotspot(); + saved_hotspot_version = VMTestAccessor::getHotspotVersion(); + saved_jvmti = VMTestAccessor::getJvmti(); + saved_structs = VMStructsTestAccessor::save(); + + g_tag = 0; + g_get_tag_calls = 0; + g_set_tag_calls = 0; + g_last_set_tag_value = -1; + g_lock_calls = 0; + g_unlock_calls = 0; + g_get_class_methods_calls = 0; + + tbl = jvmtiInterface_1_{}; + tbl.GetTag = &mock_GetTag; + tbl.SetTag = &mock_SetTag; + tbl.GetClassMethods = &mock_GetClassMethods_ok; + tbl.Deallocate = &mock_Deallocate_noop; + mock_jvmti.functions = &tbl; + + jni_tbl = JNINativeInterface_{}; + jni_tbl.GetLongField = &mock_GetLongField; + mock_jni.functions = &jni_tbl; + + fake_cld = FakePatchCLD{}; + fake_klass = FakePatchKlass{}; + fake_klass.cld = &fake_cld; + fake_klass.methods_table = &methods_header; + g_fake_klass_for_jni = &fake_klass; + + VMTestAccessor::setHotspot(true); + VMTestAccessor::setHotspotVersion(8); + VMTestAccessor::setJvmti(&mock_jvmti); + + VMStructsTestAccessor::State s = saved_structs; + s.has_class_loader_data = true; + s.methods_offset = (int)offsetof(FakePatchKlass, methods_table); + s.class_loader_data_offset = (int)offsetof(FakePatchKlass, cld); + s.vmklass_size = sizeof(FakePatchKlass); + s.vmcld_size = sizeof(FakePatchCLD); + s.lock_func = &noopLock; + s.unlock_func = &noopUnlock; + VMStructsTestAccessor::apply(s); + } + + void TearDown() override { + VMStructsTestAccessor::apply(saved_structs); + VMTestAccessor::setJvmti(saved_jvmti); + VMTestAccessor::setHotspotVersion(saved_hotspot_version); + VMTestAccessor::setHotspot(saved_hotspot); + g_fake_klass_for_jni = nullptr; + ProfiledThread::release(); + } + + void setMethodCount(int count) { methods_header = count; } + + bool callPatch(bool force_patch = false) { + jclass fake_jclass = reinterpret_cast(0x1); + return HotspotSupportTestAccessor::loadMethodIDsIfNeededImpl( + &mock_jvmti, reinterpret_cast(&mock_jni), fake_jclass, /*load_all=*/true, force_patch); + } +}; + +// Guard clause: a class with no methods has nothing to preallocate. Without +// this test, a mutant weakening `method_count <= 0` to `method_count < 0` +// would survive, since every other test uses a strictly positive count. +TEST_F(PatchClassLoaderDataTest, ZeroMethodCountSkipsPatchAndSetTag) { + setMethodCount(0); + + callPatch(); + + EXPECT_EQ(0, g_get_tag_calls) << "must bail out before even reading the tag"; + EXPECT_EQ(0, g_lock_calls) << "a class with no methods has nothing to patch"; + EXPECT_EQ(0, methodListChainLength(fake_cld.method_list_head)); + EXPECT_EQ(0, g_set_tag_calls); +} + +// Guard clause: the JDK-8062116 workaround is JDK 8-only. Every other test +// in this fixture runs with hotspot_version()==8, so without this test a +// mutant weakening `== 8` (e.g. to `>= 8`) would survive. +TEST_F(PatchClassLoaderDataTest, NonJdk8HotspotVersionSkipsPatchEntirely) { + VMTestAccessor::setHotspotVersion(9); + setMethodCount(MethodList::SIZE); + + callPatch(); + + EXPECT_EQ(0, g_lock_calls) << "the JDK-8062116 workaround must not run on a non-JDK-8 VM"; + EXPECT_EQ(0, g_get_tag_calls); + EXPECT_EQ(0, g_set_tag_calls); + EXPECT_EQ(0, methodListChainLength(fake_cld.method_list_head)); +} + +// Guard clause: VMStructs must actually have resolved the ClassLoaderData +// layout for this HotSpot build. Every other test in this fixture sets +// has_class_loader_data=true, so without this test a mutant deleting this +// half of the `!needs_patch || !VMStructs::hasClassLoaderData()` check would +// survive. +TEST_F(PatchClassLoaderDataTest, NoClassLoaderDataSupportSkipsPatchEntirely) { + VMStructsTestAccessor::State s = VMStructsTestAccessor::save(); + s.has_class_loader_data = false; + VMStructsTestAccessor::apply(s); + setMethodCount(MethodList::SIZE); + + callPatch(); + + EXPECT_EQ(0, g_lock_calls) + << "must bail out before touching the tag or CLD when VMStructs has no CLD layout"; + EXPECT_EQ(0, g_get_tag_calls); + EXPECT_EQ(0, g_set_tag_calls); + EXPECT_EQ(0, methodListChainLength(fake_cld.method_list_head)); +} + +// The simple case: nothing patched yet (tag defaults to 0), method_count is +// an exact multiple of MethodList::SIZE so already_patched lands exactly on +// method_count -- the boundary the next test replays. +TEST_F(PatchClassLoaderDataTest, FirstCallPatchesFromZeroAndTagsMethodCount) { + setMethodCount(MethodList::SIZE); + + callPatch(); + + EXPECT_EQ(1, g_get_tag_calls); + EXPECT_EQ(1, g_set_tag_calls); + EXPECT_EQ(MethodList::SIZE, g_last_set_tag_value); + EXPECT_EQ(1, methodListChainLength(fake_cld.method_list_head)); + EXPECT_EQ(1, g_lock_calls); + EXPECT_EQ(1, g_unlock_calls); +} + +// The boundary case: a replayed call (same class, unchanged method_count) +// finds already_patched == method_count and must skip the patch block +// entirely -- no lock, no new MethodList node, no SetTag. A mutant that +// widens `method_count > already_patched` to `>=` would re-enter here. +TEST_F(PatchClassLoaderDataTest, SecondCallWithUnchangedMethodCountSkipsPatchEntirely) { + setMethodCount(MethodList::SIZE); + callPatch(); + ASSERT_EQ(1, methodListChainLength(fake_cld.method_list_head)); + ASSERT_EQ(MethodList::SIZE, g_tag); + + g_get_tag_calls = 0; + g_set_tag_calls = 0; + g_lock_calls = 0; + g_unlock_calls = 0; + + callPatch(); + + EXPECT_EQ(1, g_get_tag_calls); + EXPECT_EQ(0, g_set_tag_calls) + << "method_count == already_patched must skip the whole patch block, including SetTag"; + EXPECT_EQ(0, g_lock_calls) << "the ClassLoaderData lock must not be taken when nothing needs patching"; + EXPECT_EQ(1, methodListChainLength(fake_cld.method_list_head)) + << "no new MethodList node should be prepended when nothing changed"; +} + +// Simulates RedefineClasses/RetransformClasses re-invoking the patch on a +// class whose method_count hasn't changed since the first call -- see the +// file comment above for why force_patch must bypass the tag here. A mutant +// that let force_patch fall through to the GetTag-based skip would leave +// this second call a no-op -- exactly the bug this test guards against. +TEST_F(PatchClassLoaderDataTest, ForcePatchRepatchesEvenWithUnchangedMethodCount) { + setMethodCount(MethodList::SIZE); + callPatch(/*force_patch=*/false); + ASSERT_EQ(1, methodListChainLength(fake_cld.method_list_head)); + ASSERT_EQ(MethodList::SIZE, g_tag); + + g_get_tag_calls = 0; + g_set_tag_calls = 0; + g_lock_calls = 0; + g_unlock_calls = 0; + + callPatch(/*force_patch=*/true); + + EXPECT_EQ(0, g_get_tag_calls) + << "force_patch must bypass the persisted tag entirely, not just override its outcome"; + EXPECT_EQ(1, g_lock_calls) << "a forced re-patch must take the ClassLoaderData lock"; + EXPECT_EQ(2, methodListChainLength(fake_cld.method_list_head)) + << "a forced re-patch must prepend fresh capacity even though method_count is unchanged"; + EXPECT_EQ(1, g_set_tag_calls); + EXPECT_EQ(MethodList::SIZE, g_last_set_tag_value); +} + +// Simulates a ClassPrepare replay where the class gained methods since the +// last patch (e.g. RetransformClasses): the second call must patch only the +// new tail [already_patched, method_count), not restart from 0. A mutant +// that restarts the loop at 0 would prepend 3 nodes on the second call +// (ceil(20/8)) instead of 2 (ceil((20-8)/8)), re-walking the already-patched +// [0, 8) range. +TEST_F(PatchClassLoaderDataTest, SecondCallPatchesOnlyNewMethodRange) { + setMethodCount(MethodList::SIZE); // 8 + callPatch(); + ASSERT_EQ(1, methodListChainLength(fake_cld.method_list_head)); + ASSERT_EQ(MethodList::SIZE, g_tag); + + setMethodCount(2 * MethodList::SIZE + 4); // 20 + g_set_tag_calls = 0; + + callPatch(); + + EXPECT_EQ(1, g_set_tag_calls); + EXPECT_EQ(2 * MethodList::SIZE + MethodList::SIZE, g_last_set_tag_value); // 24: next block boundary past 20 + EXPECT_EQ(3, methodListChainLength(fake_cld.method_list_head)) + << "second call must add exactly ceil((20-8)/8) = 2 new nodes on top of the first call's 1"; +} + +// Degraded case: VMKlass::classLoaderData() returns null (e.g. a klass whose +// CLD pointer field hasn't settled yet). patchClassLoaderData() must bail out +// before taking the CLD lock or touching the tag -- there is nothing to patch +// and nothing to record. A mutant that removed the cld==nullptr guard would +// dereference a null FakePatchCLD* here and crash under the gtest crash +// handler installed at file scope. +TEST_F(PatchClassLoaderDataTest, NullClassLoaderDataSkipsPatchAndSetTag) { + fake_klass.cld = nullptr; + setMethodCount(MethodList::SIZE); + + callPatch(); + + EXPECT_EQ(0, g_lock_calls) << "no ClassLoaderData to lock when cld is null"; + EXPECT_EQ(0, g_unlock_calls); + EXPECT_EQ(0, g_set_tag_calls) << "nothing was patched, so no tag should be persisted"; +} + +// Degraded case: GetTag fails but still writes a garbage value through +// *tag_ptr. patchClassLoaderData() must not trust that value -- it has to +// fall back to already_patched=0 and patch the class from scratch, the same +// as if it had never been tagged. A mutant that used the garbage tag_ptr +// value regardless of the return code would treat 999999 as "already fully +// patched" and skip patching (and prepend 0 nodes) instead of 1. +TEST_F(PatchClassLoaderDataTest, GetTagFailureIgnoresGarbageAndRestartsFromZero) { + tbl.GetTag = &mock_GetTag_garbage_on_error; + setMethodCount(MethodList::SIZE); + + callPatch(); + + EXPECT_EQ(1, g_get_tag_calls); + EXPECT_EQ(1, g_lock_calls) + << "a failed GetTag must not be mistaken for method_count <= already_patched"; + EXPECT_EQ(1, methodListChainLength(fake_cld.method_list_head)) + << "patching must restart from 0, not from the garbage *tag_ptr value"; + EXPECT_EQ(MethodList::SIZE, g_last_set_tag_value); +} + +// Same as GetTagFailureIgnoresGarbageAndRestartsFromZero, but with a +// different jvmtiError so the fallback isn't pinned to +// JVMTI_ERROR_INVALID_OBJECT specifically -- see +// mock_GetTag_wrong_phase_with_garbage's comment. +TEST_F(PatchClassLoaderDataTest, GetTagFailureWithDifferentErrorCodeStillRestartsFromZero) { + tbl.GetTag = &mock_GetTag_wrong_phase_with_garbage; + setMethodCount(MethodList::SIZE); + + callPatch(); + + EXPECT_EQ(1, g_get_tag_calls); + EXPECT_EQ(1, g_lock_calls) + << "any GetTag failure, not just JVMTI_ERROR_INVALID_OBJECT, must restart from 0"; + EXPECT_EQ(1, methodListChainLength(fake_cld.method_list_head)) + << "patching must restart from 0, not from the garbage *tag_ptr value"; + EXPECT_EQ(MethodList::SIZE, g_last_set_tag_value); +} + +// Degraded case: VM::jvmti() is null (e.g. torn down mid-shutdown). +// patchClassLoaderData() still preallocates capacity defensively -- the CLD +// lock and MethodList prepend do not depend on JVMTI at all -- but it must +// skip GetTag/SetTag entirely rather than dereferencing a null jvmtiEnv*. A +// mutant that dropped the jvmti==nullptr checks would crash calling +// GetTag/SetTag through a null functions table. +TEST_F(PatchClassLoaderDataTest, NullVMJvmtiStillPatchesButSkipsSetTag) { + VMTestAccessor::setJvmti(nullptr); + setMethodCount(MethodList::SIZE); + + callPatch(); + + EXPECT_EQ(0, g_get_tag_calls) << "GetTag must not be attempted when VM::jvmti() is null"; + EXPECT_EQ(1, g_lock_calls) + << "the patch itself must still run even when the tag can't be read or written"; + EXPECT_EQ(1, methodListChainLength(fake_cld.method_list_head)); + EXPECT_EQ(0, g_set_tag_calls) << "SetTag must be skipped when VM::jvmti() is null"; +} + +// Degraded case: SetTag() itself fails after a successful patch (e.g. +// JVMTI_ERROR_INVALID_CLASS during unload, or an env/capability mismatch). +// The tag-based dedup in patchClassLoaderData() depends entirely on this +// SetTag succeeding: a silently dropped tag means the next ClassPrepare +// replay still finds GetTag reporting the pre-patch value and re-prepends +// the same MethodList blocks -- the unbounded growth this workaround exists +// to prevent (see loadMethodIDsIfNeededImpl()'s comment on why the error is +// now logged instead of ignored). A SetTag failure must not be mistaken for +// a correctness failure, though: loadMethodIDsIfNeededImpl() reports +// whatever loadMethodIDsImpl() returned, independent of this SetTag path. +TEST_F(PatchClassLoaderDataTest, SetTagFailureIsSurvivedAndDoesNotAffectLoadedResult) { + setMethodCount(MethodList::SIZE); + tbl.SetTag = &mock_SetTag_fails; + + bool loaded = callPatch(); + + EXPECT_TRUE(loaded) << "a SetTag failure must not affect loadMethodIDsIfNeededImpl()'s result"; + EXPECT_EQ(1, g_set_tag_calls) << "SetTag must still be attempted with the freshly-patched tag"; + EXPECT_EQ(MethodList::SIZE, g_last_set_tag_value); + EXPECT_EQ(1, methodListChainLength(fake_cld.method_list_head)); + + // The failed SetTag never actually updated the JVM-side tag (g_tag is + // still 0, as SetUp() left it), so a replay must not be fooled into + // thinking this class is already patched. + g_lock_calls = 0; + g_set_tag_calls = 0; + tbl.SetTag = &mock_SetTag; + + EXPECT_TRUE(callPatch()); + EXPECT_EQ(1, g_lock_calls) << "the replay must patch from scratch, not skip as already-patched"; + EXPECT_EQ(1, g_set_tag_calls); + EXPECT_EQ(2, methodListChainLength(fake_cld.method_list_head)); +} + +#ifdef __FAULT_INJECTION__ +// --------------------------------------------------------------------------- +// patchClassLoaderData() crash-protection regression test. +// +// LockState pairs cld->lock()/unlock() with the function's sigsetjmp so a +// fault while the lock is held still unlocks it (see LockState::reset() and +// the sigsetjmp branch at the top of patchClassLoaderData()). +// INJECT_CRASH_LIKELY() sits right after state.lock(cld) and before the +// MethodList-prepend loop specifically to exercise that window, but it +// compiles to `((void)0)` outside __FAULT_INJECTION__ builds +// (-PenableFaultInjection) -- so none of the tests above, which all run in +// the default build, ever reach it. Without a test here, dropping +// state.reset(), swapping the restore()/reset() order, or changing the +// recovery path's return value all pass the rest of this file unnoticed. +// --------------------------------------------------------------------------- + +static SigAction g_orig_patch_segv_handler = nullptr; + +static void patchCrashSignalHandler(int signo, siginfo_t* siginfo, void* context) { + // patchClassLoaderData()'s sigsetjmp landing branch calls + // SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP() to compensate for the + // SignalHandlerScope destructor that the siglongjmp below bypasses (see + // guards.h) -- mirroring segvHandler's own SIGNAL_HANDLER_GUARD_NO_SAMPLE() + // is required so that compensation has something to undo, or + // exitSignalScope() trips its "Unmatched exitSignalScope" assert. + SIGNAL_HANDLER_GUARD_NO_SAMPLE(); + Profiler::checkFault(ProfiledThread::current(), siginfo, context); // siglongjmp if protected + // Not protected, or PC outside the profiler's range: not our injected fault. + SIGNAL_HANDLER_GUARD_RELEASE(); + if (g_orig_patch_segv_handler != nullptr) { + g_orig_patch_segv_handler(signo, siginfo, context); + } else { + gtestCrashHandler(signo, siginfo, context, HOTSPOT_SUPPORT_TEST_NAME); + } +} + +class PatchClassLoaderDataCrashTest : public PatchClassLoaderDataTest { +protected: + void SetUp() override { + PatchClassLoaderDataTest::SetUp(); + ProfiledThread::initCurrentThread(); + g_orig_patch_segv_handler = OS::replaceSigsegvHandler(patchCrashSignalHandler); + } + + void TearDown() override { + OS::replaceSigsegvHandler(g_orig_patch_segv_handler); + PatchClassLoaderDataTest::TearDown(); + } +}; + +// Retries the patch call with a fixed RNG seed until some INJECT_FAULT_* site +// reachable while cld's lock is held fires -- the same retry-until-fired +// pattern as WalkVmSigsetjmpRecoversFromInjectedFault in faultInjection_ut.cpp. +// g_tag is reset before every attempt so each one looks like a fresh, +// never-patched class regardless of how many prior attempts succeeded -- +// otherwise a successful attempt's SetTag would leave the class looking +// already-patched and the next attempt would skip the lock/loop entirely, +// never reaching a fault site again. +// +// Detection deliberately does not use the global FAULTS_INJECTED counter: +// VMStructs::at() (vmStructs.inline.h) also wraps its return in +// INJECT_FAULT_ADDRESS_RARE, and at() is on the hot path here too (inside +// classLoaderData(), methodCount(), and methodList()) sharing the same +// per-thread RNG stream as INJECT_CRASH_LIKELY(). Across thousands of +// retries that RARE draw can fire on its own, most often inside +// classLoaderData() *before* state.lock(cld) ever runs -- a real fault, but +// the wrong window for this test (LockState::reset() is a no-op there, +// since _cld is still null) that would otherwise register as a false +// positive and corrupt the assertions below. A successful (non-crashing) +// call always leaves g_set_tag_calls == 1 once the lock is taken (new_tag +// derived from a lock/loop that actually completed is always >= 0 here); +// landing on the lock having been taken but no tag persisted can only mean +// the sigsetjmp recovery path ran between lock() and the tag write -- +// exactly the window this test targets, regardless of which INJECT_FAULT_* +// site inside that window happened to fire. +TEST_F(PatchClassLoaderDataCrashTest, FaultBetweenLockAndResetStillUnlocksAndCanRetryFromScratch) { + setMethodCount(MethodList::SIZE); + ProfiledThread::current()->setFiRng(0xC0FFEEC0FFEEULL); + + bool faulted = false; + for (int i = 0; i < 5000 && !faulted; i++) { + g_tag = 0; + g_lock_calls = 0; + g_unlock_calls = 0; + g_set_tag_calls = 0; + callPatch(); + if (g_lock_calls == 1 && g_set_tag_calls == 0) { + faulted = true; + } + } + ASSERT_TRUE(faulted) << "expected a fault to land between lock() and the tag write within 5000 retries"; + + EXPECT_EQ(1, g_lock_calls) << "the faulted call must still have taken the lock"; + EXPECT_EQ(1, g_unlock_calls) + << "the sigsetjmp recovery path's state.reset() must unlock cld exactly once"; + EXPECT_EQ(0, g_set_tag_calls) + << "a faulted patch returns -1 and must never persist a tag for capacity that was " + "never fully prepended"; + + // No tag was persisted for the faulted attempt (g_tag is still 0, as reset + // above), so a follow-up call must not think the class is already patched: + // it re-locks and rebuilds capacity from scratch, exactly like + // FirstCallPatchesFromZeroAndTagsMethodCount. Retried the same way as the + // fault-finding loop above: this call shares the same per-thread RNG + // stream, so it can rarely hit the same RARE at() draw itself. Recording + // chain_before_retry fresh on each attempt (rather than once before the + // loop) keeps the "+1 node" check below correct even if an earlier + // attempt in this loop faulted and left no node behind. + bool retry_clean = false; + int chain_before_retry = 0; + for (int j = 0; j < 20 && !retry_clean; j++) { + chain_before_retry = methodListChainLength(fake_cld.method_list_head); + g_lock_calls = 0; + g_unlock_calls = 0; + g_set_tag_calls = 0; + callPatch(); + if (g_set_tag_calls == 1) { + retry_clean = true; + } + } + ASSERT_TRUE(retry_clean) << "expected a clean (non-faulted) retry within 20 attempts"; + + EXPECT_EQ(1, g_lock_calls) << "the retry must patch from scratch, not skip as already-patched"; + EXPECT_EQ(1, g_unlock_calls); + EXPECT_EQ(1, g_set_tag_calls); + EXPECT_EQ(MethodList::SIZE, g_last_set_tag_value); + EXPECT_EQ(chain_before_retry + 1, methodListChainLength(fake_cld.method_list_head)) + << "the retry must prepend exactly one fresh MethodList node, same as any " + "from-scratch patch"; +} +#endif // __FAULT_INJECTION__