From c31b243eeec02f38526d90e56d14585d901ffcb9 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 13:17:30 +0000 Subject: [PATCH 01/19] Missing libgcc_s.so.1 on the host should not result in crash --- ddprof-lib/src/main/cpp/profiler.cpp | 16 ++++++++++------ ddprof-lib/src/main/cpp/profiler.h | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 7c631b002..dcb4e5d29 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -843,7 +843,7 @@ void Profiler::writeHeapUsage(long value, bool live) { _locks[lock_index].unlock(); } -void Profiler::prewarmUnwinder() { +bool Profiler::prewarmUnwinder() { #ifdef __linux__ // J9 on aarch64 (and other JVMs) lazily loads libgcc_s.so.1 from its DWARF // unwinder during stack walks. When that happens inside a signal handler @@ -864,7 +864,9 @@ void Profiler::prewarmUnwinder() { // dlopen by SONAME is the only mechanism that works under static-libgcc. // libgcc_s.so.1 has been the stable SONAME since 2002; a bump would // constitute a glibc/GCC C++ ABI break and is treated as a fixed contract. - (void)dlopen("libgcc_s.so.1", RTLD_LAZY | RTLD_GLOBAL); + return dlopen("libgcc_s.so.1", RTLD_LAZY | RTLD_GLOBAL) != nullptr; +#else + return true; #endif } @@ -1306,6 +1308,12 @@ Error Profiler::checkState() { Error Profiler::init() { MutexLocker ml(_state_lock); + // Force libgcc_s to load now (idempotent dlopen) so the JVM's DWARF + // unwinder cannot lazy-load it later from signal context. + if (!prewarmUnwinder()) { + return Error("Missing libgcc_s.so"); + } + State s = state(); if (s == ERROR) { return Error("Profiler encountered fatal error"); @@ -1336,10 +1344,6 @@ Error Profiler::start(Arguments &args, bool reset) { return error; } - // Force libgcc_s to load now (idempotent dlopen) so the JVM's DWARF - // unwinder cannot lazy-load it later from signal context. - prewarmUnwinder(); - error = checkJvmCapabilities(); if (error) { return error; diff --git a/ddprof-lib/src/main/cpp/profiler.h b/ddprof-lib/src/main/cpp/profiler.h index 946c4c1e6..3e4ffffce 100644 --- a/ddprof-lib/src/main/cpp/profiler.h +++ b/ddprof-lib/src/main/cpp/profiler.h @@ -152,7 +152,7 @@ class alignas(alignof(SpinLock)) Profiler { void **_dlopen_entry; static void *dlopen_hook(const char *filename, int flags); void switchLibraryTrap(bool enable); - static void prewarmUnwinder(); + static bool prewarmUnwinder(); void disableEngines(); From 54e0fc26dd20935f883e82016fbd73e9424efdf4 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 14:46:21 +0000 Subject: [PATCH 02/19] Added fault-injection for test --- ddprof-lib/src/main/cpp/faultInjection.h | 30 ++++++++++++++ ddprof-lib/src/main/cpp/profiler.cpp | 17 +++++--- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 41 +++++++++++++++++++ 3 files changed, 82 insertions(+), 6 deletions(-) diff --git a/ddprof-lib/src/main/cpp/faultInjection.h b/ddprof-lib/src/main/cpp/faultInjection.h index e19f4bb1e..a88dbe5c4 100644 --- a/ddprof-lib/src/main/cpp/faultInjection.h +++ b/ddprof-lib/src/main/cpp/faultInjection.h @@ -28,6 +28,12 @@ // pc = SafeAccess::load(INJECT_FAULT_ADDRESS_LIKELY((void**)fp)); // VMMethod* m = ((VMMethod**)INJECT_FAULT_ADDRESS_UNLIKELY(fp))[off]; // +// INJECT_FAULT_BOOL_* wraps the *result* of a call that already ran for +// real, forcing it to report `false` so a caller's failure-handling path +// (not its memory-safety recovery path) gets exercised, e.g.: +// +// return INJECT_FAULT_BOOL_LIKELY(dlopen(name, flags) != nullptr); +// // The three tiers name their firing frequency: RARE 0.01%, UNLIKELY 0.1%, // LIKELY 1%. See faultInjection.cpp for the poison-address and PRNG details. @@ -76,6 +82,19 @@ inline T injectAddress(T ptr, u64 threshold, const char* fn) { } return ptr; } + +// Returns orig unchanged, or `faulty` when the tier fires. Unlike +// injectAddress() (which fakes an input about to be dereferenced), this fakes +// the *outcome* of a call that already ran for real — e.g. making a +// successful dlopen() appear to have failed, to exercise a caller's error +// path without needing the library to actually be absent. +template +inline T injectValue(T orig, T faulty, u64 threshold, const char* fn) { + if (__builtin_expect(shouldFire(threshold, fn), 0)) { + return faulty; + } + return orig; +} } // namespace faultinj #define INJECT_FAULT_ADDRESS_RARE(ptr) \ @@ -85,6 +104,13 @@ inline T injectAddress(T ptr, u64 threshold, const char* fn) { #define INJECT_FAULT_ADDRESS_LIKELY(ptr) \ ::faultinj::injectAddress((ptr), ::faultinj::PROB_LIKELY, __func__) +#define INJECT_FAULT_BOOL_RARE(v) \ + ::faultinj::injectValue((v), false, ::faultinj::PROB_RARE, __func__) +#define INJECT_FAULT_BOOL_UNLIKELY(v) \ + ::faultinj::injectValue((v), false, ::faultinj::PROB_UNLIKELY, __func__) +#define INJECT_FAULT_BOOL_LIKELY(v) \ + ::faultinj::injectValue((v), false, ::faultinj::PROB_LIKELY, __func__) + #else // __FAULT_INJECTION__ not defined — strict identity, zero cost. #define INJECT_FAULT_ADDRESS_RARE(ptr) (ptr) @@ -99,6 +125,10 @@ inline T injectAddress(T ptr, u64 threshold, const char* fn) { #define INJECT_FAULT_LONG_UNLIKELY(v) (v) #define INJECT_FAULT_LONG_LIKELY(v) (v) +#define INJECT_FAULT_BOOL_RARE(v) (v) +#define INJECT_FAULT_BOOL_UNLIKELY(v) (v) +#define INJECT_FAULT_BOOL_LIKELY(v) (v) + #endif // __FAULT_INJECTION__ #endif // _FAULT_INJECTION_H diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index dcb4e5d29..52fabb63f 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -864,7 +864,11 @@ bool Profiler::prewarmUnwinder() { // dlopen by SONAME is the only mechanism that works under static-libgcc. // libgcc_s.so.1 has been the stable SONAME since 2002; a bump would // constitute a glibc/GCC C++ ABI break and is treated as a fixed contract. - return dlopen("libgcc_s.so.1", RTLD_LAZY | RTLD_GLOBAL) != nullptr; + // + // INJECT_FAULT_BOOL_LIKELY lets fault-injection builds force this to + // report failure without the library actually being absent, so + // checkState()'s "Missing libgcc_s.so" path can be exercised in CI. + return INJECT_FAULT_BOOL_LIKELY(dlopen("libgcc_s.so.1", RTLD_LAZY | RTLD_GLOBAL) != nullptr); #else return true; #endif @@ -1289,6 +1293,12 @@ void Profiler::check_JDK_8313796_workaround() { } Error Profiler::checkState() { + // Force libgcc_s to load now (idempotent dlopen) so the JVM's DWARF + // unwinder cannot lazy-load it later from signal context. + if (!prewarmUnwinder()) { + return Error("Missing libgcc_s.so"); + } + State s = state(); if (s == ERROR) { return Error("Profiler encountered fatal error"); @@ -1308,11 +1318,6 @@ Error Profiler::checkState() { Error Profiler::init() { MutexLocker ml(_state_lock); - // Force libgcc_s to load now (idempotent dlopen) so the JVM's DWARF - // unwinder cannot lazy-load it later from signal context. - if (!prewarmUnwinder()) { - return Error("Missing libgcc_s.so"); - } State s = state(); if (s == ERROR) { diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index d1d25c250..f8d3b6c93 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -13,6 +13,7 @@ #include "faultInjection.h" #include "safeAccess.h" #include "os.h" +#include "profiler.h" #include "threadLocalData.h" #include "hotspot/hotspotSupport.h" #include "../../main/cpp/gtest_crash_handler.h" @@ -180,4 +181,44 @@ TEST_F(FaultInjectionTest, WalkVmSigsetjmpRecoversFromInjectedFault) { SUCCEED(); } +// Friend of Profiler (see profiler.h) — lets this test force the internal +// state to IDLE so checkState() can be exercised deterministically without a +// live JVM (matches the pattern in jvmSupport_ut.cpp). +class ProfilerTestAccessor { +public: + static void setState(Profiler* p, State s) { + p->_state.store(s, std::memory_order_release); + } +}; + +// (d) Value-injection path: PROF-15395 fixed Profiler::checkState() (shared by +// start()/check(), and therefore also reached by the -agentpath auto-start +// path) to fail cleanly instead of crashing later when libgcc_s.so.1 can't be +// loaded. libgcc_s.so.1 is always present in this test environment, so +// INJECT_FAULT_BOOL_LIKELY on prewarmUnwinder()'s return value is what makes +// that failure path reachable here: the real dlopen() still runs and +// succeeds, but the caller is deterministically told it failed. +TEST_F(FaultInjectionTest, CheckStateSurfacesInjectedPrewarmUnwinderFailure) { + Profiler* p = Profiler::instance(); + ProfilerTestAccessor::setState(p, IDLE); + ProfiledThread::current()->setFiRng(0x5EED5EED5EED5EEDULL); + + bool sawInjectedFailure = false; + bool sawClean = false; + for (int i = 0; i < 5000 && !sawInjectedFailure; i++) { + Error error = p->checkState(); + if (error) { + EXPECT_STREQ("Missing libgcc_s.so", error.message()); + sawInjectedFailure = true; + } else { + sawClean = true; + } + } + + EXPECT_TRUE(sawInjectedFailure) + << "expected at least one injected prewarmUnwinder() failure within 5000 tries"; + EXPECT_TRUE(sawClean) + << "expected at least one non-injected call to succeed (LIKELY tier is ~1%)"; +} + #endif // __FAULT_INJECTION__ From aebb2f4a8b9235dfc1b8c7a07e0ec03b69eb5c08 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 16:53:39 +0200 Subject: [PATCH 03/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index f8d3b6c93..d293218ac 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -189,6 +189,9 @@ class ProfilerTestAccessor { static void setState(Profiler* p, State s) { p->_state.store(s, std::memory_order_release); } + static State getState(Profiler* p) { + return p->_state.load(std::memory_order_acquire); + } }; // (d) Value-injection path: PROF-15395 fixed Profiler::checkState() (shared by From d97b57270b63f7c8fcc2f4935180371f0b77fcde Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 14:57:59 +0000 Subject: [PATCH 04/19] Test for injection bool values --- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index f8d3b6c93..36276009d 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -48,6 +48,18 @@ TEST(FaultInjectionTest, DisabledValueMacrosAreIdentity) { EXPECT_EQ(INJECT_FAULT_LONG_RARE(l), l); EXPECT_EQ(INJECT_FAULT_LONG_UNLIKELY(l), l); EXPECT_EQ(INJECT_FAULT_LONG_LIKELY(l), l); + + // BOOL must be identity both ways -- an accidental non-identity expansion + // (e.g. always forcing false) would otherwise only show up as a silent + // behavioural change in a production build, never a compile error. + bool t = true; + bool f = false; + EXPECT_EQ(INJECT_FAULT_BOOL_RARE(t), t); + EXPECT_EQ(INJECT_FAULT_BOOL_UNLIKELY(t), t); + EXPECT_EQ(INJECT_FAULT_BOOL_LIKELY(t), t); + EXPECT_EQ(INJECT_FAULT_BOOL_RARE(f), f); + EXPECT_EQ(INJECT_FAULT_BOOL_UNLIKELY(f), f); + EXPECT_EQ(INJECT_FAULT_BOOL_LIKELY(f), f); } #else // __FAULT_INJECTION__ enabled (built under -PenableFaultInjection) From b10ec14d882a3cc57aab248cbee7f546ef3967c6 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 15:53:09 +0000 Subject: [PATCH 05/19] Avoid repreated check --- ddprof-lib/src/main/cpp/profiler.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 52fabb63f..0a4f9ab3a 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -1293,12 +1293,6 @@ void Profiler::check_JDK_8313796_workaround() { } Error Profiler::checkState() { - // Force libgcc_s to load now (idempotent dlopen) so the JVM's DWARF - // unwinder cannot lazy-load it later from signal context. - if (!prewarmUnwinder()) { - return Error("Missing libgcc_s.so"); - } - State s = state(); if (s == ERROR) { return Error("Profiler encountered fatal error"); @@ -1310,6 +1304,13 @@ Error Profiler::checkState() { _state.store(ERROR, std::memory_order_release); return Error("Profiler encountered fatal error"); } + + // Force libgcc_s to load now (idempotent dlopen) so the JVM's DWARF + // unwinder cannot lazy-load it later from signal context. + if (!prewarmUnwinder()) { + _state.store(ERROR, std::memory_order_release); + return Error("Missing libgcc_s.so"); + } } else if (s > IDLE) { return Error("Profiler already started"); } From 03931c22c8a674447e3dc2024d92f6f4767a075d Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 16:00:00 +0000 Subject: [PATCH 06/19] Exclude test on none linux platforms --- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index ccfa641b0..a3aea86b9 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -214,6 +214,7 @@ class ProfilerTestAccessor { // that failure path reachable here: the real dlopen() still runs and // succeeds, but the caller is deterministically told it failed. TEST_F(FaultInjectionTest, CheckStateSurfacesInjectedPrewarmUnwinderFailure) { +#ifdef __linux__ Profiler* p = Profiler::instance(); ProfilerTestAccessor::setState(p, IDLE); ProfiledThread::current()->setFiRng(0x5EED5EED5EED5EEDULL); @@ -234,6 +235,7 @@ TEST_F(FaultInjectionTest, CheckStateSurfacesInjectedPrewarmUnwinderFailure) { << "expected at least one injected prewarmUnwinder() failure within 5000 tries"; EXPECT_TRUE(sawClean) << "expected at least one non-injected call to succeed (LIKELY tier is ~1%)"; +#endif // __linux__ } #endif // __FAULT_INJECTION__ From 0fd9d2d5fe02f1586f143dedc40b04567d323ffd Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 16:41:05 +0000 Subject: [PATCH 07/19] Fix test --- ddprof-lib/src/main/cpp/jvmThread.h | 2 ++ ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 35 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/ddprof-lib/src/main/cpp/jvmThread.h b/ddprof-lib/src/main/cpp/jvmThread.h index 2f5bd6910..4233a1aa0 100644 --- a/ddprof-lib/src/main/cpp/jvmThread.h +++ b/ddprof-lib/src/main/cpp/jvmThread.h @@ -15,6 +15,8 @@ * JVMThread represents a native JVM thread that is JVM implementation agnostic */ class JVMThread { + friend class JVMThreadTestAccessor; + private: static jfieldID _tid; static ThreadLocal _jvm_thread; diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index a3aea86b9..784d06197 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -15,6 +15,7 @@ #include "os.h" #include "profiler.h" #include "threadLocalData.h" +#include "jvmThread.h" #include "hotspot/hotspotSupport.h" #include "../../main/cpp/gtest_crash_handler.h" @@ -194,8 +195,8 @@ TEST_F(FaultInjectionTest, WalkVmSigsetjmpRecoversFromInjectedFault) { } // Friend of Profiler (see profiler.h) — lets this test force the internal -// state to IDLE so checkState() can be exercised deterministically without a -// live JVM (matches the pattern in jvmSupport_ut.cpp). +// state to a known value so checkState() can be exercised deterministically +// (matches the pattern in jvmSupport_ut.cpp). class ProfilerTestAccessor { public: static void setState(Profiler* p, State s) { @@ -206,6 +207,30 @@ class ProfilerTestAccessor { } }; +// Friend of JVMThread — lets this test satisfy JVMSupport::initialize()'s +// JVMThread::isInitialized() check without a live JVM attached, so +// checkState()'s NEW branch falls through to prewarmUnwinder() instead of +// latching ERROR on the JVMSupport::initialize() gate first. This binary has +// no JVMTI/JNI environment to drive JVMThread::initialize() for real, so we +// fake what a live JVM would have already set up: a discoverable pthread key +// holding a per-thread marker, exactly what ThreadLocal::initialize +// scans for. +class JVMThreadTestAccessor { +public: + static void forceInitialized() { + static jfieldID dummy_tid = reinterpret_cast(0x1); // never dereferenced here + JVMThread::_tid = dummy_tid; + if (JVMThread::_jvm_thread.isKeyValid()) { + return; + } + static void* marker = ▮ + pthread_key_t key; + ASSERT_EQ(pthread_key_create(&key, nullptr), 0); + ASSERT_EQ(pthread_setspecific(key, marker), 0); + ASSERT_TRUE(JVMThread::_jvm_thread.initialize(marker)); + } +}; + // (d) Value-injection path: PROF-15395 fixed Profiler::checkState() (shared by // start()/check(), and therefore also reached by the -agentpath auto-start // path) to fail cleanly instead of crashing later when libgcc_s.so.1 can't be @@ -216,7 +241,11 @@ class ProfilerTestAccessor { TEST_F(FaultInjectionTest, CheckStateSurfacesInjectedPrewarmUnwinderFailure) { #ifdef __linux__ Profiler* p = Profiler::instance(); - ProfilerTestAccessor::setState(p, IDLE); + // checkState() only calls prewarmUnwinder() from the NEW state, after + // JVMSupport::initialize() succeeds -- IDLE falls through checkState() + // untouched and never reaches prewarmUnwinder() at all. + JVMThreadTestAccessor::forceInitialized(); + ProfilerTestAccessor::setState(p, NEW); ProfiledThread::current()->setFiRng(0x5EED5EED5EED5EEDULL); bool sawInjectedFailure = false; From 30eb44c946655a2aa37323235228327b0ba271e1 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 19:20:38 +0200 Subject: [PATCH 08/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index 784d06197..be5633c3c 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -223,11 +223,25 @@ class JVMThreadTestAccessor { if (JVMThread::_jvm_thread.isKeyValid()) { return; } - static void* marker = ▮ + + static int marker_storage; + void* marker = &marker_storage; + pthread_key_t key; - ASSERT_EQ(pthread_key_create(&key, nullptr), 0); - ASSERT_EQ(pthread_setspecific(key, marker), 0); - ASSERT_TRUE(JVMThread::_jvm_thread.initialize(marker)); + int rc = pthread_key_create(&key, nullptr); + if (rc != 0) { + ADD_FAILURE() << "pthread_key_create failed: " << rc; + return; + } + rc = pthread_setspecific(key, marker); + if (rc != 0) { + ADD_FAILURE() << "pthread_setspecific failed: " << rc; + return; + } + if (!JVMThread::_jvm_thread.initialize(marker)) { + ADD_FAILURE() << "ThreadLocal::initialize failed"; + return; + } } }; From bbf8bf8fbf91c802ea89e35e340631c5a6bf99ab Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 19:27:01 +0200 Subject: [PATCH 09/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index be5633c3c..efe16e6d9 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -278,7 +278,7 @@ TEST_F(FaultInjectionTest, CheckStateSurfacesInjectedPrewarmUnwinderFailure) { << "expected at least one injected prewarmUnwinder() failure within 5000 tries"; EXPECT_TRUE(sawClean) << "expected at least one non-injected call to succeed (LIKELY tier is ~1%)"; -#endif // __linux__ + ProfilerTestAccessor::setState(p, NEW); } #endif // __FAULT_INJECTION__ From ae7a0423b37e6344ce30a70e0d2a8c7345cec0c8 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 19:27:35 +0200 Subject: [PATCH 10/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index efe16e6d9..fe79397a3 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -218,8 +218,8 @@ class ProfilerTestAccessor { class JVMThreadTestAccessor { public: static void forceInitialized() { - static jfieldID dummy_tid = reinterpret_cast(0x1); // never dereferenced here - JVMThread::_tid = dummy_tid; + static char dummy_tid_storage; + JVMThread::_tid = reinterpret_cast(&dummy_tid_storage); if (JVMThread::_jvm_thread.isKeyValid()) { return; } From 1652bc7198988cf5a7e41f50898be32054e3c1cf Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 17:36:28 +0000 Subject: [PATCH 11/19] More test fix --- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index 784d06197..fbc5db9d1 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -250,11 +250,17 @@ TEST_F(FaultInjectionTest, CheckStateSurfacesInjectedPrewarmUnwinderFailure) { bool sawInjectedFailure = false; bool sawClean = false; - for (int i = 0; i < 5000 && !sawInjectedFailure; i++) { + // shouldFire() mixes the fixed RNG seed above with an ASLR-dependent + // per-call-site address, so which outcome the *first* call produces is not + // deterministic run to run -- the injected failure can land before a clean + // call is observed. Keep iterating (and un-latching the ERROR state that a + // failure leaves behind) until both outcomes have been seen at least once. + for (int i = 0; i < 5000 && !(sawInjectedFailure && sawClean); i++) { Error error = p->checkState(); if (error) { EXPECT_STREQ("Missing libgcc_s.so", error.message()); sawInjectedFailure = true; + ProfilerTestAccessor::setState(p, NEW); } else { sawClean = true; } From 18aafc6fd33f01ebe1e4379b2aa39fc324154f01 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 19:38:16 +0200 Subject: [PATCH 12/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index e16e4b09d..32260b874 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -218,8 +218,10 @@ class ProfilerTestAccessor { class JVMThreadTestAccessor { public: static void forceInitialized() { - static char dummy_tid_storage; - JVMThread::_tid = reinterpret_cast(&dummy_tid_storage); + static char dummy_tid_storage; + if (JVMThread::_tid == nullptr) { + JVMThread::_tid = reinterpret_cast(&dummy_tid_storage); + } if (JVMThread::_jvm_thread.isKeyValid()) { return; } From 49cc0dfed5376d9895e2c52f9bfc0b3be3109878 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 18:41:27 +0000 Subject: [PATCH 13/19] Fix --- ddprof-lib/src/main/cpp/jvmThread.h | 2 - ddprof-lib/src/main/cpp/profiler.cpp | 14 +-- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 101 +++++++++--------- ddprof-lib/src/test/cpp/jvmSupport_ut.cpp | 16 ++- 4 files changed, 74 insertions(+), 59 deletions(-) diff --git a/ddprof-lib/src/main/cpp/jvmThread.h b/ddprof-lib/src/main/cpp/jvmThread.h index 4233a1aa0..2f5bd6910 100644 --- a/ddprof-lib/src/main/cpp/jvmThread.h +++ b/ddprof-lib/src/main/cpp/jvmThread.h @@ -15,8 +15,6 @@ * JVMThread represents a native JVM thread that is JVM implementation agnostic */ class JVMThread { - friend class JVMThreadTestAccessor; - private: static jfieldID _tid; static ThreadLocal _jvm_thread; diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 0a4f9ab3a..ebf55a26a 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -1297,6 +1297,13 @@ Error Profiler::checkState() { if (s == ERROR) { return Error("Profiler encountered fatal error"); } else if (s == NEW) { + // Force libgcc_s to load now (idempotent dlopen) so the JVM's DWARF + // unwinder cannot lazy-load it later from signal context. + if (!prewarmUnwinder()) { + _state.store(ERROR, std::memory_order_release); + return Error("Missing libgcc_s.so"); + } + // Make sure JVMSupport is initialized // In theory, it should be initialized in JVMTI::VMInit() callback, // but the callback arrives too late, after this method is called. @@ -1304,13 +1311,6 @@ Error Profiler::checkState() { _state.store(ERROR, std::memory_order_release); return Error("Profiler encountered fatal error"); } - - // Force libgcc_s to load now (idempotent dlopen) so the JVM's DWARF - // unwinder cannot lazy-load it later from signal context. - if (!prewarmUnwinder()) { - _state.store(ERROR, std::memory_order_release); - return Error("Missing libgcc_s.so"); - } } else if (s > IDLE) { return Error("Profiler already started"); } diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index e16e4b09d..ca7e82e78 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -10,12 +10,14 @@ #include #include +#include + #include "faultInjection.h" #include "safeAccess.h" #include "os.h" #include "profiler.h" #include "threadLocalData.h" -#include "jvmThread.h" +#include "vmEntry.h" #include "hotspot/hotspotSupport.h" #include "../../main/cpp/gtest_crash_handler.h" @@ -207,42 +209,39 @@ class ProfilerTestAccessor { } }; -// Friend of JVMThread — lets this test satisfy JVMSupport::initialize()'s -// JVMThread::isInitialized() check without a live JVM attached, so -// checkState()'s NEW branch falls through to prewarmUnwinder() instead of -// latching ERROR on the JVMSupport::initialize() gate first. This binary has -// no JVMTI/JNI environment to drive JVMThread::initialize() for real, so we -// fake what a live JVM would have already set up: a discoverable pthread key -// holding a per-thread marker, exactly what ThreadLocal::initialize -// scans for. -class JVMThreadTestAccessor { +// Friend of VM (see vmEntry.h) — lets this test install a mock jvmtiEnv, the +// same seam jvmSupport_ut.cpp uses. checkState() (below) checks +// prewarmUnwinder() before JVMSupport::initialize(), so the injected-failure +// path never touches this at all; it exists only so the ~99% non-injected +// iterations, which do fall through into JVMSupport::initialize(), fail +// gracefully instead of crashing on a null VM::_jvmti in this no-live-JVM +// binary. Unlike a JVMThread-level fake (which would permanently flip +// JVMThread::isInitialized() for the rest of the process, since ThreadLocal +// pthread keys are never invalidated), this is a plain pointer swap that +// ScopedJvmtiMock restores on scope exit -- no state leaks into later tests. +class VMTestAccessor { public: - static void forceInitialized() { - static char dummy_tid_storage; - JVMThread::_tid = reinterpret_cast(&dummy_tid_storage); - if (JVMThread::_jvm_thread.isKeyValid()) { - return; - } + static jvmtiEnv* getJvmti() { return VM::_jvmti; } + static void setJvmti(jvmtiEnv* env) { VM::_jvmti = env; } +}; - static int marker_storage; - void* marker = &marker_storage; +static jvmtiError JNICALL mock_GetCurrentThread_fails(jvmtiEnv*, jthread*) { + return JVMTI_ERROR_INTERNAL; +} - pthread_key_t key; - int rc = pthread_key_create(&key, nullptr); - if (rc != 0) { - ADD_FAILURE() << "pthread_key_create failed: " << rc; - return; - } - rc = pthread_setspecific(key, marker); - if (rc != 0) { - ADD_FAILURE() << "pthread_setspecific failed: " << rc; - return; - } - if (!JVMThread::_jvm_thread.initialize(marker)) { - ADD_FAILURE() << "ThreadLocal::initialize failed"; - return; - } +class ScopedJvmtiMock { +public: + ScopedJvmtiMock() : _orig(VMTestAccessor::getJvmti()) { + _tbl.GetCurrentThread = &mock_GetCurrentThread_fails; + _env.functions = &_tbl; + VMTestAccessor::setJvmti(&_env); } + ~ScopedJvmtiMock() { VMTestAccessor::setJvmti(_orig); } + +private: + jvmtiInterface_1_ _tbl{}; + _jvmtiEnv _env{}; + jvmtiEnv* _orig; }; // (d) Value-injection path: PROF-15395 fixed Profiler::checkState() (shared by @@ -255,36 +254,40 @@ class JVMThreadTestAccessor { TEST_F(FaultInjectionTest, CheckStateSurfacesInjectedPrewarmUnwinderFailure) { #ifdef __linux__ Profiler* p = Profiler::instance(); - // checkState() only calls prewarmUnwinder() from the NEW state, after - // JVMSupport::initialize() succeeds -- IDLE falls through checkState() - // untouched and never reaches prewarmUnwinder() at all. - JVMThreadTestAccessor::forceInitialized(); + // checkState() checks prewarmUnwinder() before JVMSupport::initialize(), so + // reaching the injected-failure path below needs nothing but the NEW state. + ScopedJvmtiMock jvmti_mock; ProfilerTestAccessor::setState(p, NEW); ProfiledThread::current()->setFiRng(0x5EED5EED5EED5EEDULL); bool sawInjectedFailure = false; - bool sawClean = false; + bool sawNonInjectedPrewarm = false; // shouldFire() mixes the fixed RNG seed above with an ASLR-dependent // per-call-site address, so which outcome the *first* call produces is not - // deterministic run to run -- the injected failure can land before a clean - // call is observed. Keep iterating (and un-latching the ERROR state that a - // failure leaves behind) until both outcomes have been seen at least once. - for (int i = 0; i < 5000 && !(sawInjectedFailure && sawClean); i++) { + // deterministic run to run -- the injected failure can land before a + // non-injected call is observed. Keep iterating (and un-latching the ERROR + // state that every outcome here leaves behind) until both have been seen. + for (int i = 0; i < 5000 && !(sawInjectedFailure && sawNonInjectedPrewarm); i++) { Error error = p->checkState(); - if (error) { - EXPECT_STREQ("Missing libgcc_s.so", error.message()); + ASSERT_TRUE((bool)error) << "checkState() must fail here: either the " + "injected prewarmUnwinder() failure or the " + "mocked JVMSupport::initialize() failure"; + if (std::strcmp(error.message(), "Missing libgcc_s.so") == 0) { sawInjectedFailure = true; - ProfilerTestAccessor::setState(p, NEW); } else { - sawClean = true; + // prewarmUnwinder() succeeded (non-injected, ~99% of calls) and fell + // through to the mocked JVMSupport::initialize() failure instead. + EXPECT_STREQ("Profiler encountered fatal error", error.message()); + sawNonInjectedPrewarm = true; } + ProfilerTestAccessor::setState(p, NEW); } EXPECT_TRUE(sawInjectedFailure) << "expected at least one injected prewarmUnwinder() failure within 5000 tries"; - EXPECT_TRUE(sawClean) - << "expected at least one non-injected call to succeed (LIKELY tier is ~1%)"; - ProfilerTestAccessor::setState(p, NEW); + EXPECT_TRUE(sawNonInjectedPrewarm) + << "expected at least one non-injected prewarmUnwinder() success within 5000 tries"; +#endif // __linux__ } #endif // __FAULT_INJECTION__ diff --git a/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp b/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp index 6557567eb..0c3b95f19 100644 --- a/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp +++ b/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp @@ -4,6 +4,7 @@ */ #include +#include #include "jvmSupport.h" #include "jvmThread.h" #include "vmEntry.h" @@ -111,7 +112,20 @@ TEST_F(JvmSupportInitFailureTest, JVMSupportInitializeFailsWhenJVMThreadFails) { TEST_F(JvmSupportInitFailureTest, CheckStateBlocksOnInitFailureAndLatchesError) { Profiler* p = Profiler::instance(); - Error error = p->checkState(); + // Under -PenableFaultInjection, checkState() checks prewarmUnwinder() + // before JVMSupport::initialize() (see profiler.cpp), so an injected + // fault could occasionally surface "Missing libgcc_s.so" here instead of + // the JVMSupport::initialize() failure this test targets. Retry past any + // such spurious injected failure -- a single-iteration no-op in the + // default build, where prewarmUnwinder() always succeeds. + Error error = Error::OK; + for (int i = 0; i < 100; i++) { + error = p->checkState(); + if (strcmp(error.message(), "Missing libgcc_s.so") != 0) { + break; + } + ProfilerTestAccessor::setState(p, NEW); + } bool has_error = (bool)error; EXPECT_TRUE(has_error); From 44dfc15958d0b26b58c32ec80cbc8b6a53167850 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 20:42:33 +0200 Subject: [PATCH 14/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index 32260b874..64723acb6 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -287,6 +287,7 @@ TEST_F(FaultInjectionTest, CheckStateSurfacesInjectedPrewarmUnwinderFailure) { EXPECT_TRUE(sawClean) << "expected at least one non-injected call to succeed (LIKELY tier is ~1%)"; ProfilerTestAccessor::setState(p, NEW); +#endif // __linux__ } #endif // __FAULT_INJECTION__ From a309403ac3d13a67b4241043a8c5267621f6f2df Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Mon, 27 Jul 2026 21:17:06 +0200 Subject: [PATCH 15/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/test/cpp/jvmSupport_ut.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp b/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp index 0c3b95f19..1612b4aac 100644 --- a/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp +++ b/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp @@ -114,7 +114,7 @@ TEST_F(JvmSupportInitFailureTest, CheckStateBlocksOnInitFailureAndLatchesError) // Under -PenableFaultInjection, checkState() checks prewarmUnwinder() // before JVMSupport::initialize() (see profiler.cpp), so an injected - // fault could occasionally surface "Missing libgcc_s.so" here instead of + // fault could occasionally surface "Missing libgcc_s.so.1" here instead of // the JVMSupport::initialize() failure this test targets. Retry past any // such spurious injected failure -- a single-iteration no-op in the // default build, where prewarmUnwinder() always succeeds. From e2320debcc4622dc5be982836d6a6eca8f420198 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Tue, 28 Jul 2026 14:26:43 +0200 Subject: [PATCH 16/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/test/cpp/jvmSupport_ut.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp b/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp index 1612b4aac..97e11cf1b 100644 --- a/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp +++ b/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp @@ -114,18 +114,21 @@ TEST_F(JvmSupportInitFailureTest, CheckStateBlocksOnInitFailureAndLatchesError) // Under -PenableFaultInjection, checkState() checks prewarmUnwinder() // before JVMSupport::initialize() (see profiler.cpp), so an injected - // fault could occasionally surface "Missing libgcc_s.so.1" here instead of + // fault could occasionally surface "Missing libgcc_s.so" here instead of // the JVMSupport::initialize() failure this test targets. Retry past any - // such spurious injected failure -- a single-iteration no-op in the - // default build, where prewarmUnwinder() always succeeds. + // such injected failure. If it persists across retries, libgcc_s is likely + // genuinely absent on this host and this test cannot exercise the intended path. Error error = Error::OK; for (int i = 0; i < 100; i++) { error = p->checkState(); - if (strcmp(error.message(), "Missing libgcc_s.so") != 0) { + if (!error || std::strcmp(error.message(), "Missing libgcc_s.so") != 0) { break; } ProfilerTestAccessor::setState(p, NEW); } + if (error && std::strcmp(error.message(), "Missing libgcc_s.so") == 0) { + GTEST_SKIP() << "libgcc_s.so.1 is missing on this host; cannot exercise JVMSupport::initialize() failure path"; + } bool has_error = (bool)error; EXPECT_TRUE(has_error); From 81320ae067659fd8844fde3647dd87f1ba54b6fd Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Tue, 28 Jul 2026 18:53:42 +0000 Subject: [PATCH 17/19] Fix error message --- ddprof-lib/src/main/cpp/profiler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index ebf55a26a..32ff6e21c 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -1301,7 +1301,7 @@ Error Profiler::checkState() { // unwinder cannot lazy-load it later from signal context. if (!prewarmUnwinder()) { _state.store(ERROR, std::memory_order_release); - return Error("Missing libgcc_s.so"); + return Error("Missing libgcc_s.so.1"); } // Make sure JVMSupport is initialized From 78f4b2f19d36be398e57480d5931725934635487 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Tue, 28 Jul 2026 19:06:09 +0000 Subject: [PATCH 18/19] Fix test --- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index ca7e82e78..e3d94f8e1 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -272,7 +272,7 @@ TEST_F(FaultInjectionTest, CheckStateSurfacesInjectedPrewarmUnwinderFailure) { ASSERT_TRUE((bool)error) << "checkState() must fail here: either the " "injected prewarmUnwinder() failure or the " "mocked JVMSupport::initialize() failure"; - if (std::strcmp(error.message(), "Missing libgcc_s.so") == 0) { + if (std::strcmp(error.message(), "Missing libgcc_s.so.1") == 0) { sawInjectedFailure = true; } else { // prewarmUnwinder() succeeded (non-injected, ~99% of calls) and fell From d71431ed868dbaeb259f4e9837662e26ce432d93 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Tue, 28 Jul 2026 15:51:42 -0400 Subject: [PATCH 19/19] Update test --- ddprof-lib/src/test/cpp/jvmSupport_ut.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp b/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp index 97e11cf1b..c203d296e 100644 --- a/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp +++ b/ddprof-lib/src/test/cpp/jvmSupport_ut.cpp @@ -121,12 +121,12 @@ TEST_F(JvmSupportInitFailureTest, CheckStateBlocksOnInitFailureAndLatchesError) Error error = Error::OK; for (int i = 0; i < 100; i++) { error = p->checkState(); - if (!error || std::strcmp(error.message(), "Missing libgcc_s.so") != 0) { + if (!error || std::strcmp(error.message(), "Missing libgcc_s.so.1") != 0) { break; } ProfilerTestAccessor::setState(p, NEW); } - if (error && std::strcmp(error.message(), "Missing libgcc_s.so") == 0) { + if (error && std::strcmp(error.message(), "Missing libgcc_s.so.1") == 0) { GTEST_SKIP() << "libgcc_s.so.1 is missing on this host; cannot exercise JVMSupport::initialize() failure path"; } bool has_error = (bool)error;