From 05bde602c3b67ee1cfceb809700942e3ad346f5d Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Tue, 14 Jul 2026 15:13:13 +0000 Subject: [PATCH 01/13] Investigate and implement fault injection for Java profiler --- .../native/config/ConfigurationPresets.kt | 24 +++ .../native/gtest/GtestTaskBuilder.kt | 7 + ddprof-lib/src/main/cpp/faultInjection.cpp | 111 +++++++++++ ddprof-lib/src/main/cpp/faultInjection.h | 124 ++++++++++++ .../src/main/cpp/hotspot/hotspotSupport.cpp | 15 +- ddprof-lib/src/main/cpp/hotspot/vmStructs.h | 6 +- ddprof-lib/src/main/cpp/profiler.cpp | 6 + ddprof-lib/src/main/cpp/stackWalker.cpp | 9 +- ddprof-lib/src/main/cpp/threadLocalData.h | 28 ++- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 184 ++++++++++++++++++ 10 files changed, 500 insertions(+), 14 deletions(-) create mode 100644 ddprof-lib/src/main/cpp/faultInjection.cpp create mode 100644 ddprof-lib/src/main/cpp/faultInjection.h create mode 100644 ddprof-lib/src/test/cpp/faultInjection_ut.cpp diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt index 681f76a1bc..66d41489c1 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt @@ -53,6 +53,14 @@ object ConfigurationPresets { register("fuzzer") { configureFuzzer(this, currentPlatform, currentArch, version, rootDir, compiler) } + // Opt-in fault-injection config: a release build with -D__FAULT_INJECTION__. + // Only registered when -PenableFaultInjection is passed, so normal + // builds never see the define. + if (project.hasProperty("enableFaultInjection")) { + register("faultinjection") { + configureFaultInjection(this, currentPlatform, currentArch, version) + } + } } val activeConfigs = extension.getActiveConfigurations(currentPlatform, currentArch) @@ -125,6 +133,22 @@ object ConfigurationPresets { } } + /** + * Fault-injection configuration: identical to release but with + * -D__FAULT_INJECTION__, which activates the compile-time fault-injection + * layer (faultInjection.h) at the profiler's memory-access sites. Opt-in + * only (see setupStandardConfigurations); never shipped in production. + */ + fun configureFaultInjection( + config: BuildConfiguration, + platform: Platform, + architecture: Architecture, + version: String + ) { + configureRelease(config, platform, architecture, version) + config.compilerArgs.add("-D__FAULT_INJECTION__") + } + fun configureDebug( config: BuildConfiguration, platform: Platform, diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestTaskBuilder.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestTaskBuilder.kt index 81d5742268..30c207d9db 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestTaskBuilder.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestTaskBuilder.kt @@ -257,6 +257,13 @@ class GtestTaskBuilder( // Mark unit-test builds so test-only production APIs are compiled in. args.add("-DUNIT_TEST") + // Mirror the opt-in fault-injection define so the fault-injection unit + // test can compile the enabled path under -PenableFaultInjection. Guard + // against duplicating it if the config already carries it. + if (project.hasProperty("enableFaultInjection") && !args.contains("-D__FAULT_INJECTION__")) { + args.add("-D__FAULT_INJECTION__") + } + return args } diff --git a/ddprof-lib/src/main/cpp/faultInjection.cpp b/ddprof-lib/src/main/cpp/faultInjection.cpp new file mode 100644 index 0000000000..0f1d2ff98c --- /dev/null +++ b/ddprof-lib/src/main/cpp/faultInjection.cpp @@ -0,0 +1,111 @@ +/* + * Copyright 2026, Datadog, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "faultInjection.h" + +// The whole translation unit is empty unless fault injection is enabled, so a +// normal build links a no-op object file. +#ifdef __FAULT_INJECTION__ + +#include "os.h" // OS::page_size +#include "threadLocalData.h" // ProfiledThread::currentSignalSafe / nextFiRandom +#include +#include + +namespace faultinj { + +// Knuth multiplicative constant (== common.h KNUTH_MULTIPLICATIVE_CONSTANT), +// used to hash the function name and to advance the global fallback PRNG. +static constexpr u64 KNUTH = 0x9e3779b97f4a7c15ULL; + +// Word-alignment mask for produced addresses. +static constexpr uintptr_t ALIGN_MASK = ~(uintptr_t)(sizeof(void*) - 1); + +// Guard region: mmapped once at init() with PROT_NONE so any access faults. +// Written only by init() (off the signal path); read-only afterwards. +static uintptr_t g_guard_base = 0; +static uintptr_t g_guard_span = 0; +static bool g_guard_ok = false; + +// Fallback PRNG for threads with no ProfiledThread context. Relaxed atomics +// keep it lock-free and async-signal-safe; a lost update on a race is harmless. +static std::atomic g_fallback_rng{KNUTH}; + +void init() { + // A handful of pages is plenty of distinct fault addresses; keep it small. + size_t span = 16 * OS::page_size; + void* p = mmap(nullptr, span, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (p == MAP_FAILED) { + g_guard_ok = false; // poisonAddress() falls back to random garbage. + return; + } + g_guard_base = (uintptr_t)p; + g_guard_span = span; + g_guard_ok = true; +} + +u64 nextRandom() { + ProfiledThread* t = ProfiledThread::currentSignalSafe(); // never allocates + if (t != nullptr) { + return t->nextFiRandom(); + } + // Fallback: relaxed atomic xorshift64. Not perfectly serialised, but the + // stream only needs to be roughly uniform for injection decisions. + u64 x = g_fallback_rng.load(std::memory_order_relaxed); + if (x == 0) x = 1; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + g_fallback_rng.store(x, std::memory_order_relaxed); + return x; +} + +bool shouldFire(u64 threshold, const char* fn) { + // XOR with a per-function hash is a bijection on 64 bits, so it perturbs the + // stream per call site while keeping the fire probability exactly + // threshold/2^64. + u64 r = nextRandom() ^ ((u64)(uintptr_t)fn * KNUTH); + return r < threshold; +} + +uintptr_t poisonAddress() { + u64 r = nextRandom(); + if (g_guard_ok && (r & 1u)) { + // Guaranteed-unmapped guard page — deterministic SIGSEGV. + return (g_guard_base + (uintptr_t)(r % g_guard_span)) & ALIGN_MASK; + } + // Random non-canonical address (high bit set keeps it well above any mapped + // low page) — may raise SIGSEGV or SIGBUS. + return ((uintptr_t)r | (uintptr_t)0x8000000000000000ULL) & ALIGN_MASK; +} + +int32_t injectInt(int32_t v, u64 threshold, const char* fn) { + if (__builtin_expect(shouldFire(threshold, fn), 0)) { + return (int32_t)nextRandom(); + } + return v; +} + +int64_t injectLong(int64_t v, u64 threshold, const char* fn) { + if (__builtin_expect(shouldFire(threshold, fn), 0)) { + return (int64_t)nextRandom(); + } + return v; +} + +} // namespace faultinj + +#endif // __FAULT_INJECTION__ diff --git a/ddprof-lib/src/main/cpp/faultInjection.h b/ddprof-lib/src/main/cpp/faultInjection.h new file mode 100644 index 0000000000..d00033d3e9 --- /dev/null +++ b/ddprof-lib/src/main/cpp/faultInjection.h @@ -0,0 +1,124 @@ +/* + * Copyright 2026, Datadog, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Compile-time fault-injection layer for the profiler's memory-access sites. +// +// The macros below wrap a pointer or value expression at a real dereference +// site (VMStructs::at, walkVM, walkFP, walkDwarf). When __FAULT_INJECTION__ is +// defined, each wrapped expression, with the tier's probability, is replaced by +// a deliberately bad address (so the load faults and the profiler's recovery +// path — SafeAccess safefetch or walkVM's setjmp/longjmp — is exercised) or a +// random int/long value. When the flag is NOT defined, every macro is a strict +// identity: it expands to exactly the parenthesized original expression, with +// unchanged type and value category and zero runtime cost. +// +// pc = SafeAccess::load(INJECT_FAULT_ADDRESS_LIKELY((void**)fp)); +// VMMethod* m = ((VMMethod**)INJECT_FAULT_ADDRESS_UNLIKELY(fp))[off]; +// +// 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. + +#ifndef _FAULT_INJECTION_H +#define _FAULT_INJECTION_H + +#ifdef __FAULT_INJECTION__ + +#include "arch.h" // u64 +#include + +namespace faultinj { + +// Firing probability expressed as an xorshift64 threshold (round(p * 2^64)), so +// the hot-path check is a single integer compare (rng < threshold) with no +// floating point in the signal handler. +constexpr u64 PROB_RARE = 1844674407370955ULL; // 1e-4 (0.01%) +constexpr u64 PROB_UNLIKELY = 18446744073709552ULL; // 1e-3 (0.1%) +constexpr u64 PROB_LIKELY = 184467440737095520ULL; // 1e-2 (1%) + +// Called once at profiler startup (off the signal path) to mmap the PROT_NONE +// guard region used by poisonAddress(). Safe to call before any injection. +void init(); + +// Draws one per-thread (or global-fallback) PRNG value. Async-signal-safe. +u64 nextRandom(); + +// Returns true with probability threshold/2^64. The function name perturbs the +// draw so distinct call sites get statistically independent decisions. +bool shouldFire(u64 threshold, const char* fn); + +// A word-aligned address guaranteed to fault on access: half the time a pointer +// into the mmap'd PROT_NONE guard region (deterministic SIGSEGV), half the time +// a random non-canonical address (SIGSEGV or SIGBUS). Arithmetic-only, no +// syscall — safe on the signal-handler hot path. +uintptr_t poisonAddress(); + +// Returns ptr unchanged, or a poison address (cast to T) when the tier fires. +// Templated so the wrapped expression's static type (void**, const char*, +// uintptr_t, ...) is preserved exactly. +template +inline T injectAddress(T ptr, u64 threshold, const char* fn) { + if (__builtin_expect(shouldFire(threshold, fn), 0)) { + // C-style cast intentionally: converts the numeric poison address to any + // pointer type or to uintptr_t. This code only ever compiles under the flag. + return (T)poisonAddress(); + } + return ptr; +} + +// Returns v unchanged, or a pseudo-random value when the tier fires. +int32_t injectInt(int32_t v, u64 threshold, const char* fn); +int64_t injectLong(int64_t v, u64 threshold, const char* fn); + +} // namespace faultinj + +#define INJECT_FAULT_ADDRESS_RARE(ptr) \ + ::faultinj::injectAddress((ptr), ::faultinj::PROB_RARE, __func__) +#define INJECT_FAULT_ADDRESS_UNLIKELY(ptr) \ + ::faultinj::injectAddress((ptr), ::faultinj::PROB_UNLIKELY, __func__) +#define INJECT_FAULT_ADDRESS_LIKELY(ptr) \ + ::faultinj::injectAddress((ptr), ::faultinj::PROB_LIKELY, __func__) + +#define INJECT_FAULT_INT_RARE(v) \ + ::faultinj::injectInt((v), ::faultinj::PROB_RARE, __func__) +#define INJECT_FAULT_INT_UNLIKELY(v) \ + ::faultinj::injectInt((v), ::faultinj::PROB_UNLIKELY, __func__) +#define INJECT_FAULT_INT_LIKELY(v) \ + ::faultinj::injectInt((v), ::faultinj::PROB_LIKELY, __func__) + +#define INJECT_FAULT_LONG_RARE(v) \ + ::faultinj::injectLong((v), ::faultinj::PROB_RARE, __func__) +#define INJECT_FAULT_LONG_UNLIKELY(v) \ + ::faultinj::injectLong((v), ::faultinj::PROB_UNLIKELY, __func__) +#define INJECT_FAULT_LONG_LIKELY(v) \ + ::faultinj::injectLong((v), ::faultinj::PROB_LIKELY, __func__) + +#else // __FAULT_INJECTION__ not defined — strict identity, zero cost. + +#define INJECT_FAULT_ADDRESS_RARE(ptr) (ptr) +#define INJECT_FAULT_ADDRESS_UNLIKELY(ptr) (ptr) +#define INJECT_FAULT_ADDRESS_LIKELY(ptr) (ptr) + +#define INJECT_FAULT_INT_RARE(v) (v) +#define INJECT_FAULT_INT_UNLIKELY(v) (v) +#define INJECT_FAULT_INT_LIKELY(v) (v) + +#define INJECT_FAULT_LONG_RARE(v) (v) +#define INJECT_FAULT_LONG_UNLIKELY(v) (v) +#define INJECT_FAULT_LONG_LIKELY(v) (v) + +#endif // __FAULT_INJECTION__ + +#endif // _FAULT_INJECTION_H diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index d0b6c48eeb..6fc905b903 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -7,6 +7,7 @@ #include #include #include "asyncSampleMutex.h" +#include "faultInjection.h" #include "frames.h" #include "guards.h" #include "hotspot/hotspotSupport.h" @@ -370,8 +371,8 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex // entry_fp has been range-checked by isValidFP above; any remaining // SIGSEGV from a stale/concurrently-freed pointer is caught by the // setjmp crash protection in walkVM (checkFault -> longjmp). - uintptr_t carrier_fp = *(uintptr_t*)entry_fp; - const void* carrier_pc = ((const void**)entry_fp)[FRAME_PC_SLOT]; + uintptr_t carrier_fp = *(uintptr_t*)INJECT_FAULT_ADDRESS_UNLIKELY(entry_fp); + const void* carrier_pc = ((const void**)INJECT_FAULT_ADDRESS_UNLIKELY(entry_fp))[FRAME_PC_SLOT]; uintptr_t carrier_sp = entry_fp + (FRAME_PC_SLOT + 1) * sizeof(void*); if (!StackWalkValidation::isValidFP(carrier_fp) || StackWalkValidation::inDeadZone(carrier_pc) || @@ -496,7 +497,7 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex bool is_plausible_interpreter_frame = StackWalkValidation::isPlausibleInterpreterFrame(fp, sp, bcp_offset); if (is_plausible_interpreter_frame) { - VMMethod* method = ((VMMethod**)fp)[InterpreterFrame::method_offset]; + VMMethod* method = ((VMMethod**)INJECT_FAULT_ADDRESS_UNLIKELY(fp))[InterpreterFrame::method_offset]; jmethodID method_id = getMethodId(method); if (method_id != JMETHODID_NOT_WALKABLE) { Counters::increment(WALKVM_JAVA_FRAME_OK); @@ -506,7 +507,7 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex fillFrame(frames[depth++], FRAME_INTERPRETED, bci, method_id, method); sp = ((uintptr_t*)fp)[InterpreterFrame::sender_sp_offset]; pc = stripPointer(((void**)fp)[FRAME_PC_SLOT]); - fp = *(uintptr_t*)fp; + fp = *(uintptr_t*)INJECT_FAULT_ADDRESS_UNLIKELY(fp); continue; } } @@ -518,7 +519,7 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex Counters::increment(WALKVM_JAVA_FRAME_OK); fillFrame(frames[depth++], FRAME_INTERPRETED, 0, method_id, method); if (is_plausible_interpreter_frame) { - pc = stripPointer(((void**)fp)[FRAME_PC_SLOT]); + pc = stripPointer(((void**)INJECT_FAULT_ADDRESS_UNLIKELY(fp))[FRAME_PC_SLOT]); sp = frame.senderSP(); fp = *(uintptr_t*)fp; } else { @@ -595,7 +596,7 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex break; } - fp = ((uintptr_t*)sp)[-FRAME_PC_SLOT - 1]; + fp = ((uintptr_t*)INJECT_FAULT_ADDRESS_UNLIKELY(sp))[-FRAME_PC_SLOT - 1]; pc = ((const void**)sp)[-FRAME_PC_SLOT]; continue; } else if (frame.unwindPrologue(nm, (uintptr_t&)pc, sp, fp)) { @@ -737,7 +738,7 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex // In HotSpot, lastJavaFP is non-zero only for interpreter frames; // compiled frames record FP=0 in the anchor. if (StackWalkValidation::isPlausibleInterpreterFrame(recovery_fp, recovery_sp, bcp_offset)) { - VMMethod* method = ((VMMethod**)recovery_fp)[InterpreterFrame::method_offset]; + VMMethod* method = ((VMMethod**)INJECT_FAULT_ADDRESS_UNLIKELY(recovery_fp))[InterpreterFrame::method_offset]; jmethodID method_id = getMethodId(method); if (method_id != JMETHODID_NOT_WALKABLE) { anchor = NULL; diff --git a/ddprof-lib/src/main/cpp/hotspot/vmStructs.h b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h index f8c97af1b7..9ef2586851 100644 --- a/ddprof-lib/src/main/cpp/hotspot/vmStructs.h +++ b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h @@ -14,6 +14,7 @@ #include #include "codeCache.h" #include "counters.h" +#include "faultInjection.h" #include "jvmThread.h" #include "safeAccess.h" #include "threadState.h" @@ -451,13 +452,14 @@ class VMStructs { const char* at(int offset) { const char* ptr = (const char*)this + offset; assert(crashProtectionActive() || SafeAccess::isReadable(ptr)); - return ptr; + // Poison only the returned pointer; the assert above sees the real ptr. + return INJECT_FAULT_ADDRESS_RARE(ptr); } const char* at(int offset) const { const char* ptr = (const char*)this + offset; assert(crashProtectionActive() || SafeAccess::isReadable(ptr)); - return ptr; + return INJECT_FAULT_ADDRESS_RARE(ptr); } static bool goodPtr(const void* ptr) { diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 05dc7ea624..8cc69c8714 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -16,6 +16,7 @@ #include "ctimer.h" #include "signalInflight.h" #include "dwarf.h" +#include "faultInjection.h" #include "flightRecorder.h" #include "itimer.h" #include "hotspot/vmStructs.inline.h" @@ -998,6 +999,11 @@ void Profiler::setupSignalHandlers() { // Patch sigaction GOT in libraries with broken signal handlers (already loaded) LibraryPatcher::patch_sigaction(); } +#ifdef __FAULT_INJECTION__ + // Reserve the PROT_NONE guard region used to poison memory-access sites. + // Done here (off the signal path) once handlers are installed. + faultinj::init(); +#endif } } diff --git a/ddprof-lib/src/main/cpp/stackWalker.cpp b/ddprof-lib/src/main/cpp/stackWalker.cpp index 68be876432..48255415fe 100644 --- a/ddprof-lib/src/main/cpp/stackWalker.cpp +++ b/ddprof-lib/src/main/cpp/stackWalker.cpp @@ -7,6 +7,7 @@ #include #include "stackWalker.inline.h" #include "dwarf.h" +#include "faultInjection.h" #include "profiler.h" #include "stackFrame.h" #include "symbols.h" @@ -60,13 +61,13 @@ int StackWalker::walkFP(void* ucontext, const void** callchain, int max_depth, S break; } - pc = stripPointer(SafeAccess::load((void**)fp + FRAME_PC_SLOT)); + pc = stripPointer(SafeAccess::load(INJECT_FAULT_ADDRESS_LIKELY((void**)fp + FRAME_PC_SLOT))); if (inDeadZone(pc)) { break; } sp = fp + (FRAME_PC_SLOT + 1) * sizeof(void*); - fp = (uintptr_t)SafeAccess::load((void**)fp); + fp = (uintptr_t)SafeAccess::load(INJECT_FAULT_ADDRESS_LIKELY((void**)fp)); } if (truncated && depth > max_depth) { @@ -145,7 +146,7 @@ int StackWalker::walkDwarf(void* ucontext, const void** callchain, int max_depth if (!aligned(fp_addr)) { break; } - fp = (uintptr_t)SafeAccess::load((void**)fp_addr); + fp = (uintptr_t)SafeAccess::load(INJECT_FAULT_ADDRESS_LIKELY((void**)fp_addr)); } if (EMPTY_FRAME_SIZE > 0 || f.pc_off != DW_LINK_REGISTER) { @@ -153,7 +154,7 @@ int StackWalker::walkDwarf(void* ucontext, const void** callchain, int max_depth if (!aligned(pc_addr)) { break; } - pc = stripPointer(SafeAccess::load((void**)pc_addr)); + pc = stripPointer(SafeAccess::load(INJECT_FAULT_ADDRESS_LIKELY((void**)pc_addr))); } else if (depth == 1) { pc = (const void*)frame.link(); } else { diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index e8b9a30c46..26e7af39d7 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -85,6 +85,11 @@ class ProfiledThread : public ThreadLocalData { uint8_t _signal_depth; // Nested signal-handler depth (see SignalHandlerScope) UnwindFailures _unwind_failures; bool _otel_ctx_initialized; +#ifdef __FAULT_INJECTION__ + // xorshift64 PRNG state for compile-time fault injection (faultInjection.h). + // Per-thread, so the signal-path draw needs no lock/atomic; must never be 0. + u64 _fi_rng; +#endif // alignas(8) + sizeof(OtelThreadContextRecord)==640 (multiple of 8) guarantee // _otel_tag_encodings sits at +640 with no padding, so the three fields form one // 688-byte contiguous region exposed as a combined DirectByteBuffer. @@ -103,7 +108,15 @@ class ProfiledThread : public ThreadLocalData { _park_block_token(0), _filter_slot_id(-1), _init_window(0), _signal_depth(0), _otel_ctx_initialized(false), - _otel_ctx_record{}, _otel_tag_encodings{}, _otel_local_root_span_id(0) {}; + _otel_ctx_record{}, _otel_tag_encodings{}, _otel_local_root_span_id(0) { +#ifdef __FAULT_INJECTION__ + // Seed like PoissonSampler: instance address XOR a hash of the tid, forced + // non-zero (0 is a fixed point of xorshift64). 0x9e37... is the Knuth + // multiplicative constant (see common.h KNUTH_MULTIPLICATIVE_CONSTANT). + _fi_rng = ((u64)(uintptr_t)this) ^ (0x9e3779b97f4a7c15ULL * (u64)tid); + if (_fi_rng == 0) _fi_rng = 1; +#endif + }; virtual ~ProfiledThread() { } public: @@ -224,6 +237,19 @@ class ProfiledThread : public ThreadLocalData { inline void enterSignalScope() { ++_signal_depth; } inline void exitSignalScope() { if (_signal_depth > 0) --_signal_depth; } +#ifdef __FAULT_INJECTION__ + // One xorshift64 step (Marsaglia 2003), matching PoissonSampler::nextExp. + // Plain member r/w is AS-safe: signals are delivered to the owning thread. + inline u64 nextFiRandom() { + _fi_rng ^= _fi_rng << 13; + _fi_rng ^= _fi_rng >> 7; + _fi_rng ^= _fi_rng << 17; + return _fi_rng; + } + // Test hook: force a deterministic PRNG stream for rate/recovery assertions. + inline void setFiRng(u64 seed) { _fi_rng = seed ? seed : 1; } +#endif + UnwindFailures* unwindFailures(bool reset = true) { if (reset) { _unwind_failures.clear(); diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp new file mode 100644 index 0000000000..c4553296dd --- /dev/null +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -0,0 +1,184 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +#include "faultInjection.h" +#include "safeAccess.h" +#include "os.h" +#include "threadLocalData.h" +#include "hotspot/hotspotSupport.h" +#include "../../main/cpp/gtest_crash_handler.h" + +static constexpr char FAULT_INJECTION_TEST_NAME[] = "FaultInjectionTest"; + +// --------------------------------------------------------------------------- +// (a) Disabled build: every macro must be a strict identity. Compiled only when +// the flag is absent (the default gtest / CI build). This is the guard that +// protects production from any behavioural change. +// --------------------------------------------------------------------------- +#ifndef __FAULT_INJECTION__ + +TEST(FaultInjectionTest, DisabledAddressMacrosAreIdentity) { + void* p = reinterpret_cast(0xCAFEBABE); + const char* cp = reinterpret_cast(0x1234); + uintptr_t up = 0xDEAD; + EXPECT_EQ(INJECT_FAULT_ADDRESS_RARE(p), p); + EXPECT_EQ(INJECT_FAULT_ADDRESS_UNLIKELY(cp), cp); + EXPECT_EQ(INJECT_FAULT_ADDRESS_LIKELY(up), up); + // Type is preserved: assigning back to the original type must compile. + void* p2 = INJECT_FAULT_ADDRESS_LIKELY(p); + EXPECT_EQ(p2, p); +} + +TEST(FaultInjectionTest, DisabledValueMacrosAreIdentity) { + int32_t i = 42; + int64_t l = 0x1122334455667788LL; + EXPECT_EQ(INJECT_FAULT_INT_RARE(i), i); + EXPECT_EQ(INJECT_FAULT_INT_UNLIKELY(i), i); + EXPECT_EQ(INJECT_FAULT_INT_LIKELY(i), i); + EXPECT_EQ(INJECT_FAULT_LONG_RARE(l), l); + EXPECT_EQ(INJECT_FAULT_LONG_UNLIKELY(l), l); + EXPECT_EQ(INJECT_FAULT_LONG_LIKELY(l), l); +} + +#else // __FAULT_INJECTION__ enabled (built under -PenableFaultInjection) + +// Chain: safefetch recovery first, then walkVM setjmp/longjmp recovery, then +// the crash handler as a last resort so a genuine bug still produces a report. +static void (*orig_segvHandler)(int, siginfo_t*, void*); +static void (*orig_busHandler)(int, siginfo_t*, void*); + +static void fi_signal_wrapper(int signo, siginfo_t* siginfo, void* context) { + if (SafeAccess::handle_safefetch(signo, context)) { + return; // safefetch load recovered; PC already rewritten to _cont. + } + HotspotSupport::checkFault(ProfiledThread::currentSignalSafe()); // longjmp if protected + // Not protected and not a safefetch fault — real crash. + if (signo == SIGBUS && orig_busHandler != nullptr) { + orig_busHandler(signo, siginfo, context); + } else if (signo == SIGSEGV && orig_segvHandler != nullptr) { + orig_segvHandler(signo, siginfo, context); + } else { + gtestCrashHandler(signo, siginfo, context, FAULT_INJECTION_TEST_NAME); + } +} + +class FaultInjectionTest : public ::testing::Test { +protected: + void SetUp() override { + ProfiledThread::initCurrentThread(); + faultinj::init(); + orig_segvHandler = OS::replaceSigsegvHandler(fi_signal_wrapper); + orig_busHandler = OS::replaceSigbusHandler(fi_signal_wrapper); + } + void TearDown() override { + OS::replaceSigsegvHandler(orig_segvHandler); + OS::replaceSigbusHandler(orig_busHandler); + ProfiledThread::release(); + } +}; + +// (b) The empirical firing rate is within a wide band of the nominal tier rate. +// Wide bounds ([0.3x, 3x]) + a fixed seed keep it deterministic and non-flaky. +static void expectRateInBand(u64 threshold, size_t n, double nominal) { + ProfiledThread::currentSignalSafe()->setFiRng(0x0123456789ABCDEFULL); + size_t fires = 0; + for (size_t i = 0; i < n; i++) { + if (faultinj::shouldFire(threshold, "rateProbe")) { + fires++; + } + } + double expected = nominal * (double)n; + EXPECT_GT((double)fires, 0.3 * expected) + << "fired " << fires << " of " << n << ", expected ~" << expected; + EXPECT_LT((double)fires, 3.0 * expected) + << "fired " << fires << " of " << n << ", expected ~" << expected; +} + +TEST_F(FaultInjectionTest, LikelyTierRate) { + expectRateInBand(faultinj::PROB_LIKELY, 1000000, 1e-2); // ~10000 fires +} +TEST_F(FaultInjectionTest, UnlikelyTierRate) { + expectRateInBand(faultinj::PROB_UNLIKELY, 10000000, 1e-3); // ~10000 fires +} +TEST_F(FaultInjectionTest, RareTierRate) { + expectRateInBand(faultinj::PROB_RARE, 100000000, 1e-4); // ~10000 fires +} + +// poisonAddress() must always yield a fault-inducing, word-aligned address. +TEST_F(FaultInjectionTest, PoisonAddressIsAlignedAndUnmapped) { + ProfiledThread::currentSignalSafe()->setFiRng(0xF00DF00DF00DF00DULL); + for (int i = 0; i < 1000; i++) { + uintptr_t bad = faultinj::poisonAddress(); + EXPECT_EQ(bad & (sizeof(void*) - 1), 0u) << "poison address not word-aligned"; + // Reading it via SafeAccess must fault-and-recover, never return readable data. + void* got = SafeAccess::load(reinterpret_cast(bad), (void*)-1); + EXPECT_EQ(got, (void*)-1) << "poison address was unexpectedly readable"; + } +} + +// (c1) SafeAccess path: injecting a poison pointer into SafeAccess::load must be +// absorbed by the safefetch handler — it returns the default and never crashes. +TEST_F(FaultInjectionTest, SafeAccessRecoversFromInjectedFault) { + void* real = reinterpret_cast(0xABCDEF00); + void** valid = ℜ + ProfiledThread::currentSignalSafe()->setFiRng(0xBEEFCAFEBEEFCAFEULL); + size_t fired = 0; + for (int i = 0; i < 200000; i++) { + // LIKELY tier: some iterations poison the address, others read `valid`. + void* got = SafeAccess::load(INJECT_FAULT_ADDRESS_LIKELY(valid), nullptr); + ASSERT_TRUE(got == real || got == nullptr) << "unexpected value " << got; + if (got == nullptr) { + fired++; // a poison address was injected and safefetch recovered. + } + } + EXPECT_GT(fired, 0u) << "expected at least one injected fault to be recovered"; +} + +// (c2) walkVM path: a raw dereference of an injected poison pointer must be +// caught by the setjmp/longjmp crash protection, returning control to setjmp. +TEST_F(FaultInjectionTest, WalkVmSetjmpRecoversFromInjectedFault) { + ProfiledThread* t = ProfiledThread::currentSignalSafe(); + ASSERT_NE(t, nullptr); + + uintptr_t real_slot = 0; // a valid, readable pointer slot + uintptr_t base = (uintptr_t)&real_slot; + volatile bool recovered = false; + volatile size_t reads = 0; + volatile size_t faults = 0; + + jmp_buf ctx; + if (setjmp(ctx) != 0) { + recovered = true; // returned here via checkFault -> longjmp + faults++; + } + t->setJmpCtx(&ctx); + t->setFiRng(0xD00DFEEDD00DFEEDULL); + + // Force at least one fire deterministically, then let the tier drive the rest. + for (int i = 0; i < 5000 && faults == 0; i++) { + // Raw deref of the (possibly poisoned) base — mirrors walkVM's raw reads. + uintptr_t v = *(uintptr_t*)INJECT_FAULT_ADDRESS_LIKELY(base); + (void)v; + reads++; + } + t->setJmpCtx(nullptr); + + // Either we observed a recovered fault, or the loop completed cleanly. The + // essential assertion is that the process did not die and, when a fault was + // injected, setjmp regained control. + if (faults > 0) { + EXPECT_TRUE(recovered); + } + SUCCEED(); +} + +#endif // __FAULT_INJECTION__ From d85f055273e38b3bd9ad5a6913516fca7d60a0f2 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Wed, 15 Jul 2026 20:08:02 +0000 Subject: [PATCH 02/13] Add counters --- .../native/config/ConfigurationPresets.kt | 33 +++++-------------- ddprof-lib/src/main/cpp/counters.h | 6 +++- ddprof-lib/src/main/cpp/faultInjection.cpp | 9 ++++- .../src/main/cpp/hotspot/hotspotSupport.cpp | 1 + ddprof-lib/src/main/cpp/safeAccess.cpp | 6 ++-- 5 files changed, 27 insertions(+), 28 deletions(-) diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt index 66d41489c1..c05b9c9ab2 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt @@ -37,12 +37,21 @@ object ConfigurationPresets { project.logger.lifecycle("Setting up standard build configurations for $currentPlatform-$currentArch") project.logger.lifecycle("Using compiler: $compiler") + // Opt-in compile-time fault injection. When -PenableFaultInjection is + // passed we append -D__FAULT_INJECTION__ to the standard release/debug + // library builds, so the documented buildRelease / buildDebug workflows + // produce a fault-injected libjavaProfiler.so. It is intentionally NOT + // applied to asan/tsan/fuzzer: those configs install their own SIGSEGV + // interception, which conflicts with the deliberately-faulting loads. + val faultInjection = project.hasProperty("enableFaultInjection") extension.buildConfigurations.apply { register("release") { configureRelease(this, currentPlatform, currentArch, version) + if (faultInjection) compilerArgs.add("-D__FAULT_INJECTION__") } register("debug") { configureDebug(this, currentPlatform, currentArch, version) + if (faultInjection) compilerArgs.add("-D__FAULT_INJECTION__") } register("asan") { configureAsan(this, currentPlatform, currentArch, version, rootDir, compiler) @@ -53,14 +62,6 @@ object ConfigurationPresets { register("fuzzer") { configureFuzzer(this, currentPlatform, currentArch, version, rootDir, compiler) } - // Opt-in fault-injection config: a release build with -D__FAULT_INJECTION__. - // Only registered when -PenableFaultInjection is passed, so normal - // builds never see the define. - if (project.hasProperty("enableFaultInjection")) { - register("faultinjection") { - configureFaultInjection(this, currentPlatform, currentArch, version) - } - } } val activeConfigs = extension.getActiveConfigurations(currentPlatform, currentArch) @@ -133,22 +134,6 @@ object ConfigurationPresets { } } - /** - * Fault-injection configuration: identical to release but with - * -D__FAULT_INJECTION__, which activates the compile-time fault-injection - * layer (faultInjection.h) at the profiler's memory-access sites. Opt-in - * only (see setupStandardConfigurations); never shipped in production. - */ - fun configureFaultInjection( - config: BuildConfiguration, - platform: Platform, - architecture: Architecture, - version: String - ) { - configureRelease(config, platform, architecture, version) - config.compilerArgs.add("-D__FAULT_INJECTION__") - } - fun configureDebug( config: BuildConfiguration, platform: Platform, diff --git a/ddprof-lib/src/main/cpp/counters.h b/ddprof-lib/src/main/cpp/counters.h index 41c26eb3c8..42862bb444 100644 --- a/ddprof-lib/src/main/cpp/counters.h +++ b/ddprof-lib/src/main/cpp/counters.h @@ -124,7 +124,11 @@ * paths (delegated and direct) go into SAMPLES_DROPPED_REC_LOCK. */ \ X(JVMTI_STACKS_DROPPED_LOCK, "jvmti_stacks_dropped_lock") \ X(SAMPLES_DROPPED_REC_LOCK, "samples_dropped_rec_lock") \ - X(SAMPLES_DROPPED_THREAD_LOCAL, "samples_dropped_thread_local") + X(SAMPLES_DROPPED_THREAD_LOCAL, "samples_dropped_thread_local") \ + X(SAFECOPY_FAILED, "safecopy_failed") \ + X(SAFEFETCH_FAILED, "safefetch_failed") \ + X(FAULTS_INJECTED, "faults_injected") \ + X(WALKVM_LONGJMP_RECOVERED, "walkvm_longjmp_recovered") #define X_ENUM(a, b) a, typedef enum CounterId : int { DD_COUNTER_TABLE(X_ENUM) DD_NUM_COUNTERS diff --git a/ddprof-lib/src/main/cpp/faultInjection.cpp b/ddprof-lib/src/main/cpp/faultInjection.cpp index 0f1d2ff98c..9515c85158 100644 --- a/ddprof-lib/src/main/cpp/faultInjection.cpp +++ b/ddprof-lib/src/main/cpp/faultInjection.cpp @@ -20,6 +20,7 @@ // normal build links a no-op object file. #ifdef __FAULT_INJECTION__ +#include "counters.h" // Counters::increment (FAULTS_INJECTED) #include "os.h" // OS::page_size #include "threadLocalData.h" // ProfiledThread::currentSignalSafe / nextFiRandom #include @@ -78,7 +79,13 @@ bool shouldFire(u64 threshold, const char* fn) { // stream per call site while keeping the fire probability exactly // threshold/2^64. u64 r = nextRandom() ^ ((u64)(uintptr_t)fn * KNUTH); - return r < threshold; + if (__builtin_expect(r < threshold, 0)) { + // Every address/int/long injection routes through here, so this is the one + // place that counts an actually-injected fault. + Counters::increment(FAULTS_INJECTED); + return true; + } + return false; } uintptr_t poisonAddress() { diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index 6fc905b903..00ca372647 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -970,6 +970,7 @@ void HotspotSupport::checkFault(ProfiledThread* thrd) { } thrd->resetCrashHandler(); + Counters::increment(WALKVM_LONGJMP_RECOVERED); longjmp(*thrd->getJmpCtx(), 1); } diff --git a/ddprof-lib/src/main/cpp/safeAccess.cpp b/ddprof-lib/src/main/cpp/safeAccess.cpp index f423af9d9c..86da26c6bc 100644 --- a/ddprof-lib/src/main/cpp/safeAccess.cpp +++ b/ddprof-lib/src/main/cpp/safeAccess.cpp @@ -16,8 +16,7 @@ #include "safeAccess.h" -#include -#include +#include "counters.h" #include #include @@ -280,14 +279,17 @@ bool SafeAccess::handle_safefetch(int sig, void* context) { if ((sig == SIGSEGV || sig == SIGBUS) && uc != nullptr) { if (pc == (uintptr_t)safefetch32_impl) { uc->current_pc = (uintptr_t)safefetch32_cont; + Counters::increment(SAFEFETCH_FAILED); return true; } else if (pc == (uintptr_t)safefetch64_impl) { uc->current_pc = (uintptr_t)safefetch64_cont; + Counters::increment(SAFEFETCH_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. uc->current_pc = (uintptr_t)safecopy_cont; + Counters::increment(SAFECOPY_FAILED); return true; } } From a12b80b3508d1b6d455205039d22844c4d4121bd Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Thu, 16 Jul 2026 14:30:48 +0200 Subject: [PATCH 03/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/main/cpp/faultInjection.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/ddprof-lib/src/main/cpp/faultInjection.cpp b/ddprof-lib/src/main/cpp/faultInjection.cpp index 9515c85158..886b526ded 100644 --- a/ddprof-lib/src/main/cpp/faultInjection.cpp +++ b/ddprof-lib/src/main/cpp/faultInjection.cpp @@ -37,25 +37,30 @@ static constexpr uintptr_t ALIGN_MASK = ~(uintptr_t)(sizeof(void*) - 1); // Guard region: mmapped once at init() with PROT_NONE so any access faults. // Written only by init() (off the signal path); read-only afterwards. -static uintptr_t g_guard_base = 0; -static uintptr_t g_guard_span = 0; -static bool g_guard_ok = false; +static std::atomic g_guard_base{0}; +static std::atomic g_guard_span{0}; +static std::atomic g_guard_ok{false}; // Fallback PRNG for threads with no ProfiledThread context. Relaxed atomics // keep it lock-free and async-signal-safe; a lost update on a race is harmless. static std::atomic g_fallback_rng{KNUTH}; void init() { + // Avoid repeated mmaps (tests call init() in each fixture SetUp()). + if (g_guard_ok.load(std::memory_order_acquire)) { + return; + } + // A handful of pages is plenty of distinct fault addresses; keep it small. size_t span = 16 * OS::page_size; void* p = mmap(nullptr, span, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (p == MAP_FAILED) { - g_guard_ok = false; // poisonAddress() falls back to random garbage. return; } - g_guard_base = (uintptr_t)p; - g_guard_span = span; - g_guard_ok = true; + + g_guard_base.store((uintptr_t)p, std::memory_order_relaxed); + g_guard_span.store(span, std::memory_order_relaxed); + g_guard_ok.store(true, std::memory_order_release); } u64 nextRandom() { From 6367efe653d5571e5467dace5713b0ca8abe0c24 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Thu, 16 Jul 2026 14:32:22 +0200 Subject: [PATCH 04/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/main/cpp/faultInjection.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ddprof-lib/src/main/cpp/faultInjection.cpp b/ddprof-lib/src/main/cpp/faultInjection.cpp index 886b526ded..672fde313a 100644 --- a/ddprof-lib/src/main/cpp/faultInjection.cpp +++ b/ddprof-lib/src/main/cpp/faultInjection.cpp @@ -95,12 +95,13 @@ bool shouldFire(u64 threshold, const char* fn) { uintptr_t poisonAddress() { u64 r = nextRandom(); - if (g_guard_ok && (r & 1u)) { + if (g_guard_ok.load(std::memory_order_acquire)) { // Guaranteed-unmapped guard page — deterministic SIGSEGV. - return (g_guard_base + (uintptr_t)(r % g_guard_span)) & ALIGN_MASK; + uintptr_t base = g_guard_base.load(std::memory_order_relaxed); + uintptr_t span = g_guard_span.load(std::memory_order_relaxed); + return (base + (uintptr_t)(r % span)) & ALIGN_MASK; } - // Random non-canonical address (high bit set keeps it well above any mapped - // low page) — may raise SIGSEGV or SIGBUS. + // Fallback: best-effort garbage address if init() failed. return ((uintptr_t)r | (uintptr_t)0x8000000000000000ULL) & ALIGN_MASK; } From f1c6dc1111fa56451484355bba2662323b3cd11b Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Thu, 16 Jul 2026 14:32:48 +0200 Subject: [PATCH 05/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/main/cpp/faultInjection.h | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ddprof-lib/src/main/cpp/faultInjection.h b/ddprof-lib/src/main/cpp/faultInjection.h index d00033d3e9..672c8a2ec6 100644 --- a/ddprof-lib/src/main/cpp/faultInjection.h +++ b/ddprof-lib/src/main/cpp/faultInjection.h @@ -59,11 +59,9 @@ u64 nextRandom(); // draw so distinct call sites get statistically independent decisions. bool shouldFire(u64 threshold, const char* fn); -// A word-aligned address guaranteed to fault on access: half the time a pointer -// into the mmap'd PROT_NONE guard region (deterministic SIGSEGV), half the time -// a random non-canonical address (SIGSEGV or SIGBUS). Arithmetic-only, no -// syscall — safe on the signal-handler hot path. -uintptr_t poisonAddress(); +// A word-aligned address intended to fault on access. When init() has reserved the +// mmap'd PROT_NONE guard region, this returns an address inside it (deterministic +// SIGSEGV). If init() failed, it falls back to a best-effort garbage address. // Returns ptr unchanged, or a poison address (cast to T) when the tier fires. // Templated so the wrapped expression's static type (void**, const char*, From b88f34ec8edeb4333ba152c49ebc16c24913e4d0 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Thu, 16 Jul 2026 14:27:07 +0000 Subject: [PATCH 06/13] New counters and missing includes --- ddprof-lib/src/main/cpp/counters.h | 28 ++++++++++++++++++++++-- ddprof-lib/src/main/cpp/faultInjection.h | 1 + ddprof-lib/src/main/cpp/safeAccess.cpp | 25 +++++++++++++++++++++ ddprof-lib/src/main/cpp/safeAccess.h | 17 ++++++++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/ddprof-lib/src/main/cpp/counters.h b/ddprof-lib/src/main/cpp/counters.h index 42862bb444..cfc34cd7cd 100644 --- a/ddprof-lib/src/main/cpp/counters.h +++ b/ddprof-lib/src/main/cpp/counters.h @@ -127,8 +127,32 @@ X(SAMPLES_DROPPED_THREAD_LOCAL, "samples_dropped_thread_local") \ X(SAFECOPY_FAILED, "safecopy_failed") \ X(SAFEFETCH_FAILED, "safefetch_failed") \ - X(FAULTS_INJECTED, "faults_injected") \ - X(WALKVM_LONGJMP_RECOVERED, "walkvm_longjmp_recovered") + X(WALKVM_LONGJMP_RECOVERED, "walkvm_longjmp_recovered") \ + DD_COUNTER_TABLE_FAULT_INJECTION(X) \ + DD_COUNTER_TABLE_DEBUG(X) + +// Fault-injection-only counter: number of faults actually injected. Only +// compiled in when __FAULT_INJECTION__ is defined, so it occupies no enum slot +// and adds no storage in normal builds. +#ifdef __FAULT_INJECTION__ +#define DD_COUNTER_TABLE_FAULT_INJECTION(X) \ + X(FAULTS_INJECTED, "faults_injected") +#else +#define DD_COUNTER_TABLE_FAULT_INJECTION(X) +#endif + +// Debug-only counters: SafeAccess reads/copies issued while the thread is +// already inside a walkVM longjmp-protected region (redundant safefetch +// overhead). Not compiled into release builds at all, so they occupy no enum +// slot and add no storage there. +#ifdef DEBUG +#define DD_COUNTER_TABLE_DEBUG(X) \ + X(SAFEFETCH_WHILE_PROTECTED, "safefetch_while_protected") \ + X(SAFECOPY_WHILE_PROTECTED, "safecopy_while_protected") +#else +#define DD_COUNTER_TABLE_DEBUG(X) +#endif + #define X_ENUM(a, b) a, typedef enum CounterId : int { DD_COUNTER_TABLE(X_ENUM) DD_NUM_COUNTERS diff --git a/ddprof-lib/src/main/cpp/faultInjection.h b/ddprof-lib/src/main/cpp/faultInjection.h index 672c8a2ec6..e17d1178be 100644 --- a/ddprof-lib/src/main/cpp/faultInjection.h +++ b/ddprof-lib/src/main/cpp/faultInjection.h @@ -62,6 +62,7 @@ bool shouldFire(u64 threshold, const char* fn); // A word-aligned address intended to fault on access. When init() has reserved the // mmap'd PROT_NONE guard region, this returns an address inside it (deterministic // SIGSEGV). If init() failed, it falls back to a best-effort garbage address. +uintptr_t poisonAddress(); // Returns ptr unchanged, or a poison address (cast to T) when the tier fires. // Templated so the wrapped expression's static type (void**, const char*, diff --git a/ddprof-lib/src/main/cpp/safeAccess.cpp b/ddprof-lib/src/main/cpp/safeAccess.cpp index 86da26c6bc..4f5cc231ef 100644 --- a/ddprof-lib/src/main/cpp/safeAccess.cpp +++ b/ddprof-lib/src/main/cpp/safeAccess.cpp @@ -17,8 +17,14 @@ #include "safeAccess.h" #include "counters.h" + +#include +#include #include #include +#ifdef DEBUG +#include "threadLocalData.h" // ProfiledThread::currentSignalSafe / isProtected +#endif extern "C" int safefetch32_cont(int* adr, int errValue); extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue); @@ -263,7 +269,20 @@ static void verify_safecopy_range() { #endif #endif +#ifdef DEBUG +void SafeAccess::countIfLongjmpProtected(bool isCopy) { + ProfiledThread* t = ProfiledThread::currentSignalSafe(); // never allocates + if (t != nullptr && t->isProtected()) { + Counters::increment(isCopy ? SAFECOPY_WHILE_PROTECTED + : SAFEFETCH_WHILE_PROTECTED); + } +} +#endif + bool SafeAccess::safeCopy(void* dst, const void* src, size_t len) { +#ifdef DEBUG + countIfLongjmpProtected(true); +#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 // redirects execution to safecopy_cont, which returns 0. Because the copy @@ -303,11 +322,17 @@ void* SafeAccess::load(void** ptr, void* default_value) { } int32_t SafeAccess::load32(int32_t* ptr, int32_t default_value) { +#ifdef DEBUG + countIfLongjmpProtected(false); +#endif int res = safefetch32_impl((int*)ptr, (int)default_value); return static_cast(res); } void* SafeAccess::loadPtr(void** ptr, void* default_value) { +#ifdef DEBUG + countIfLongjmpProtected(false); +#endif #if defined(__x86_64__) || defined(__aarch64__) int64_t res = safefetch64_impl((int64_t*)ptr, (int64_t)reinterpret_cast(default_value)); return (void*)static_cast(res); diff --git a/ddprof-lib/src/main/cpp/safeAccess.h b/ddprof-lib/src/main/cpp/safeAccess.h index 14a009aad3..7d78f2cc61 100644 --- a/ddprof-lib/src/main/cpp/safeAccess.h +++ b/ddprof-lib/src/main/cpp/safeAccess.h @@ -52,6 +52,9 @@ class SafeAccess { */ NOINLINE static int safeFetch32(int* ptr, int errorValue) { +#ifdef DEBUG + countIfLongjmpProtected(false); +#endif return safefetch32_impl(ptr, errorValue); } @@ -62,6 +65,9 @@ class SafeAccess { */ NOINLINE static int64_t safeFetch64(int64_t* ptr, int64_t errorValue) { +#ifdef DEBUG + countIfLongjmpProtected(false); +#endif return safefetch64_impl(ptr, errorValue); } @@ -109,6 +115,17 @@ class SafeAccess { } return isReadable(end_page); } + +#ifdef DEBUG +private: + // Debug diagnostic: bump a counter when a SafeAccess read/copy is issued while + // the current thread is already inside a walkVM longjmp-protected region, where + // the safefetch/safecopy overhead is redundant (a fault there is caught by the + // longjmp 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); +#endif }; #endif // _SAFEACCESS_H From 0567605c64e975a6ae2b668defdbf5ad583a99b7 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Thu, 16 Jul 2026 18:32:29 +0200 Subject: [PATCH 07/13] 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, 3 insertions(+), 3 deletions(-) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index c4553296dd..bd72651135 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -104,13 +104,13 @@ static void expectRateInBand(u64 threshold, size_t n, double nominal) { } TEST_F(FaultInjectionTest, LikelyTierRate) { - expectRateInBand(faultinj::PROB_LIKELY, 1000000, 1e-2); // ~10000 fires + expectRateInBand(faultinj::PROB_LIKELY, 200000, 1e-2); // ~2000 fires } TEST_F(FaultInjectionTest, UnlikelyTierRate) { - expectRateInBand(faultinj::PROB_UNLIKELY, 10000000, 1e-3); // ~10000 fires + expectRateInBand(faultinj::PROB_UNLIKELY, 2000000, 1e-3); // ~2000 fires } TEST_F(FaultInjectionTest, RareTierRate) { - expectRateInBand(faultinj::PROB_RARE, 100000000, 1e-4); // ~10000 fires + expectRateInBand(faultinj::PROB_RARE, 20000000, 1e-4); // ~2000 fires } // poisonAddress() must always yield a fault-inducing, word-aligned address. From 82c5d3b08e12693bfbb2ad1620dbd830cc59a855 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Thu, 16 Jul 2026 16:33:12 +0000 Subject: [PATCH 08/13] Add a new counter --- ddprof-lib/src/main/cpp/counters.h | 14 ++++++++++++++ ddprof-lib/src/main/cpp/faultInjection.cpp | 8 ++++++++ 2 files changed, 22 insertions(+) diff --git a/ddprof-lib/src/main/cpp/counters.h b/ddprof-lib/src/main/cpp/counters.h index cfc34cd7cd..24950f6bb0 100644 --- a/ddprof-lib/src/main/cpp/counters.h +++ b/ddprof-lib/src/main/cpp/counters.h @@ -129,6 +129,7 @@ X(SAFEFETCH_FAILED, "safefetch_failed") \ X(WALKVM_LONGJMP_RECOVERED, "walkvm_longjmp_recovered") \ DD_COUNTER_TABLE_FAULT_INJECTION(X) \ + DD_COUNTER_TABLE_FI_DEBUG(X) \ DD_COUNTER_TABLE_DEBUG(X) // Fault-injection-only counter: number of faults actually injected. Only @@ -141,6 +142,19 @@ #define DD_COUNTER_TABLE_FAULT_INJECTION(X) #endif +// Fault-injection + debug only: faults injected while the current thread was +// NOT inside a walkVM longjmp-protected region. Such a site relies solely on +// safefetch (or would genuinely crash if the poisoned pointer is raw-dereferenced +// outside any recovery), so a non-zero value flags injection sites that are not +// covered by longjmp protection. Compiled in only when both __FAULT_INJECTION__ +// and DEBUG are defined. +#if defined(__FAULT_INJECTION__) && defined(DEBUG) +#define DD_COUNTER_TABLE_FI_DEBUG(X) \ + X(FAULTS_INJECTED_UNPROTECTED, "faults_injected_unprotected") +#else +#define DD_COUNTER_TABLE_FI_DEBUG(X) +#endif + // Debug-only counters: SafeAccess reads/copies issued while the thread is // already inside a walkVM longjmp-protected region (redundant safefetch // overhead). Not compiled into release builds at all, so they occupy no enum diff --git a/ddprof-lib/src/main/cpp/faultInjection.cpp b/ddprof-lib/src/main/cpp/faultInjection.cpp index 672fde313a..bd21e47a6e 100644 --- a/ddprof-lib/src/main/cpp/faultInjection.cpp +++ b/ddprof-lib/src/main/cpp/faultInjection.cpp @@ -88,6 +88,14 @@ bool shouldFire(u64 threshold, const char* fn) { // Every address/int/long injection routes through here, so this is the one // place that counts an actually-injected fault. Counters::increment(FAULTS_INJECTED); +#ifdef DEBUG + // Flag injections fired at a site with no walkVM longjmp protection active: + // recovery there depends entirely on safefetch, and a raw deref would crash. + ProfiledThread* t = ProfiledThread::currentSignalSafe(); // never allocates + if (t == nullptr || !t->isProtected()) { + Counters::increment(FAULTS_INJECTED_UNPROTECTED); + } +#endif return true; } return false; From ca8ed8986656e5eef5d1519121ad48c150566c22 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Thu, 16 Jul 2026 19:31:28 +0000 Subject: [PATCH 09/13] Fix review --- ddprof-lib/src/main/cpp/safeAccess.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ddprof-lib/src/main/cpp/safeAccess.cpp b/ddprof-lib/src/main/cpp/safeAccess.cpp index 4f5cc231ef..d5ebde8a6e 100644 --- a/ddprof-lib/src/main/cpp/safeAccess.cpp +++ b/ddprof-lib/src/main/cpp/safeAccess.cpp @@ -294,6 +294,10 @@ bool SafeAccess::safeCopy(void* dst, const void* src, size_t len) { bool SafeAccess::handle_safefetch(int sig, void* context) { ucontext_t* uc = (ucontext_t*)context; + if (uc == nullptr) { + return false; + } + uintptr_t pc = uc->current_pc; if ((sig == SIGSEGV || sig == SIGBUS) && uc != nullptr) { if (pc == (uintptr_t)safefetch32_impl) { From ed8e5628724d50832a1f0d652998a9698ba809fb Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Thu, 16 Jul 2026 20:39:51 +0000 Subject: [PATCH 10/13] Cleanup --- ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp | 14 ++++++++------ ddprof-lib/src/main/cpp/safeAccess.cpp | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index 00ca372647..29449114af 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -371,8 +371,9 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex // entry_fp has been range-checked by isValidFP above; any remaining // SIGSEGV from a stale/concurrently-freed pointer is caught by the // setjmp crash protection in walkVM (checkFault -> longjmp). - uintptr_t carrier_fp = *(uintptr_t*)INJECT_FAULT_ADDRESS_UNLIKELY(entry_fp); - const void* carrier_pc = ((const void**)INJECT_FAULT_ADDRESS_UNLIKELY(entry_fp))[FRAME_PC_SLOT]; + uintptr_t* carrier_fp_addr = (uintptr_t*)INJECT_FAULT_ADDRESS_UNLIKELY(entry_fp); + uintptr_t carrier_fp = *carrier_fp_addr; + const void* carrier_pc = ((const void**)carrier_fp_addr)[FRAME_PC_SLOT]; uintptr_t carrier_sp = entry_fp + (FRAME_PC_SLOT + 1) * sizeof(void*); if (!StackWalkValidation::isValidFP(carrier_fp) || StackWalkValidation::inDeadZone(carrier_pc) || @@ -519,9 +520,10 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex Counters::increment(WALKVM_JAVA_FRAME_OK); fillFrame(frames[depth++], FRAME_INTERPRETED, 0, method_id, method); if (is_plausible_interpreter_frame) { - pc = stripPointer(((void**)INJECT_FAULT_ADDRESS_UNLIKELY(fp))[FRAME_PC_SLOT]); + uintptr_t* fp_addr = (uintptr_t*)INJECT_FAULT_ADDRESS_UNLIKELY(fp); + pc = stripPointer(((void**)fp_addr)[FRAME_PC_SLOT]); sp = frame.senderSP(); - fp = *(uintptr_t*)fp; + fp = *fp_addr; } else { pc = stripPointer(SafeAccess::load((void**)sp)); sp = frame.senderSP(); @@ -595,8 +597,8 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex fillFrame(frames[depth++], BCI_ERROR, "break_misaligned_sp"); break; } - - fp = ((uintptr_t*)INJECT_FAULT_ADDRESS_UNLIKELY(sp))[-FRAME_PC_SLOT - 1]; + sp = (uintptr_t)INJECT_FAULT_ADDRESS_UNLIKELY(sp); + fp = ((uintptr_t*)sp)[-FRAME_PC_SLOT - 1]; pc = ((const void**)sp)[-FRAME_PC_SLOT]; continue; } else if (frame.unwindPrologue(nm, (uintptr_t&)pc, sp, fp)) { diff --git a/ddprof-lib/src/main/cpp/safeAccess.cpp b/ddprof-lib/src/main/cpp/safeAccess.cpp index d5ebde8a6e..af6d47d572 100644 --- a/ddprof-lib/src/main/cpp/safeAccess.cpp +++ b/ddprof-lib/src/main/cpp/safeAccess.cpp @@ -299,7 +299,7 @@ bool SafeAccess::handle_safefetch(int sig, void* context) { } uintptr_t pc = uc->current_pc; - if ((sig == SIGSEGV || sig == SIGBUS) && uc != nullptr) { + if (sig == SIGSEGV || sig == SIGBUS) { if (pc == (uintptr_t)safefetch32_impl) { uc->current_pc = (uintptr_t)safefetch32_cont; Counters::increment(SAFEFETCH_FAILED); From f97ecb66b81a7afd336e2b970983b58804bf7e1b Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 17 Jul 2026 13:05:08 +0000 Subject: [PATCH 11/13] Removed unused --- ddprof-lib/src/main/cpp/faultInjection.cpp | 15 --------------- ddprof-lib/src/main/cpp/faultInjection.h | 19 ------------------- ddprof-lib/src/test/cpp/faultInjection_ut.cpp | 7 +++---- 3 files changed, 3 insertions(+), 38 deletions(-) diff --git a/ddprof-lib/src/main/cpp/faultInjection.cpp b/ddprof-lib/src/main/cpp/faultInjection.cpp index bd21e47a6e..1572037063 100644 --- a/ddprof-lib/src/main/cpp/faultInjection.cpp +++ b/ddprof-lib/src/main/cpp/faultInjection.cpp @@ -112,21 +112,6 @@ uintptr_t poisonAddress() { // Fallback: best-effort garbage address if init() failed. return ((uintptr_t)r | (uintptr_t)0x8000000000000000ULL) & ALIGN_MASK; } - -int32_t injectInt(int32_t v, u64 threshold, const char* fn) { - if (__builtin_expect(shouldFire(threshold, fn), 0)) { - return (int32_t)nextRandom(); - } - return v; -} - -int64_t injectLong(int64_t v, u64 threshold, const char* fn) { - if (__builtin_expect(shouldFire(threshold, fn), 0)) { - return (int64_t)nextRandom(); - } - return v; -} - } // namespace faultinj #endif // __FAULT_INJECTION__ diff --git a/ddprof-lib/src/main/cpp/faultInjection.h b/ddprof-lib/src/main/cpp/faultInjection.h index e17d1178be..f3e5ad1b64 100644 --- a/ddprof-lib/src/main/cpp/faultInjection.h +++ b/ddprof-lib/src/main/cpp/faultInjection.h @@ -76,11 +76,6 @@ inline T injectAddress(T ptr, u64 threshold, const char* fn) { } return ptr; } - -// Returns v unchanged, or a pseudo-random value when the tier fires. -int32_t injectInt(int32_t v, u64 threshold, const char* fn); -int64_t injectLong(int64_t v, u64 threshold, const char* fn); - } // namespace faultinj #define INJECT_FAULT_ADDRESS_RARE(ptr) \ @@ -90,20 +85,6 @@ int64_t injectLong(int64_t v, u64 threshold, const char* fn); #define INJECT_FAULT_ADDRESS_LIKELY(ptr) \ ::faultinj::injectAddress((ptr), ::faultinj::PROB_LIKELY, __func__) -#define INJECT_FAULT_INT_RARE(v) \ - ::faultinj::injectInt((v), ::faultinj::PROB_RARE, __func__) -#define INJECT_FAULT_INT_UNLIKELY(v) \ - ::faultinj::injectInt((v), ::faultinj::PROB_UNLIKELY, __func__) -#define INJECT_FAULT_INT_LIKELY(v) \ - ::faultinj::injectInt((v), ::faultinj::PROB_LIKELY, __func__) - -#define INJECT_FAULT_LONG_RARE(v) \ - ::faultinj::injectLong((v), ::faultinj::PROB_RARE, __func__) -#define INJECT_FAULT_LONG_UNLIKELY(v) \ - ::faultinj::injectLong((v), ::faultinj::PROB_UNLIKELY, __func__) -#define INJECT_FAULT_LONG_LIKELY(v) \ - ::faultinj::injectLong((v), ::faultinj::PROB_LIKELY, __func__) - #else // __FAULT_INJECTION__ not defined — strict identity, zero cost. #define INJECT_FAULT_ADDRESS_RARE(ptr) (ptr) diff --git a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp index bd72651135..6059163b73 100644 --- a/ddprof-lib/src/test/cpp/faultInjection_ut.cpp +++ b/ddprof-lib/src/test/cpp/faultInjection_ut.cpp @@ -172,12 +172,11 @@ TEST_F(FaultInjectionTest, WalkVmSetjmpRecoversFromInjectedFault) { } t->setJmpCtx(nullptr); - // Either we observed a recovered fault, or the loop completed cleanly. The + // We should have observed a recovered fault, or the loop completed cleanly. The // essential assertion is that the process did not die and, when a fault was // injected, setjmp regained control. - if (faults > 0) { - EXPECT_TRUE(recovered); - } + EXPECT_GT(faults, 0u) << "expected at least one injected fault to longjmp-recover"; + EXPECT_TRUE(recovered); SUCCEED(); } From 1a40bf511be91cf23122c605a14ae219130806e3 Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 17 Jul 2026 15:27:39 +0000 Subject: [PATCH 12/13] Disable fault-injection for ASAN/TSAN --- .../datadoghq/native/gtest/GtestTaskBuilder.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestTaskBuilder.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestTaskBuilder.kt index 30c207d9db..2c8151f62c 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestTaskBuilder.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestTaskBuilder.kt @@ -257,12 +257,15 @@ class GtestTaskBuilder( // Mark unit-test builds so test-only production APIs are compiled in. args.add("-DUNIT_TEST") - // Mirror the opt-in fault-injection define so the fault-injection unit - // test can compile the enabled path under -PenableFaultInjection. Guard - // against duplicating it if the config already carries it. - if (project.hasProperty("enableFaultInjection") && !args.contains("-D__FAULT_INJECTION__")) { - args.add("-D__FAULT_INJECTION__") - } + // NOTE: -D__FAULT_INJECTION__ is intentionally NOT added here. It is + // applied per-config in ConfigurationPresets (release/debug only) and we + // start from config.compilerArgs.get() above, so the release/debug gtest + // builds inherit it automatically under -PenableFaultInjection while the + // asan/tsan/fuzzer configs correctly never receive it. Re-deriving it + // from the project property here would be redundant for release/debug + // (they already carry it) and would wrongly leak the deliberately- + // faulting loads into the sanitizer configs, which install their own + // SIGSEGV interception. return args } From 2577ae53a428fadda1f208450213d5984f4b637e Mon Sep 17 00:00:00 2001 From: Zhengyu Gu Date: Fri, 17 Jul 2026 18:17:21 +0000 Subject: [PATCH 13/13] Initializing Counters eagerly --- ddprof-lib/src/main/cpp/profiler.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 8cc69c8714..d1a432f227 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -987,6 +987,17 @@ int Profiler::crashHandlerInternal(int signo, siginfo_t *siginfo, void *ucontext void Profiler::setupSignalHandlers() { // Do not re-run the signal setup (run only when VM has not been loaded yet) if (__sync_bool_compare_and_swap(&_signals_initialized, false, true)) { + // 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 + // WALKVM_* 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 + // already-allocated array. + (void)Counters::getCounters(); + if (VM::isHotspot() || VM::isOpenJ9()) { // HotSpot and J9 tolerate interposed SIGSEGV/SIGBUS handler; other JVMs probably not // IMPORTANT: protectSignalHandlers must be called BEFORE replaceSigsegvHandler so that