Skip to content

Commit 05bde60

Browse files
committed
Investigate and implement fault injection for Java profiler
1 parent 4e0e178 commit 05bde60

10 files changed

Lines changed: 500 additions & 14 deletions

File tree

build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ object ConfigurationPresets {
5353
register("fuzzer") {
5454
configureFuzzer(this, currentPlatform, currentArch, version, rootDir, compiler)
5555
}
56+
// Opt-in fault-injection config: a release build with -D__FAULT_INJECTION__.
57+
// Only registered when -PenableFaultInjection is passed, so normal
58+
// builds never see the define.
59+
if (project.hasProperty("enableFaultInjection")) {
60+
register("faultinjection") {
61+
configureFaultInjection(this, currentPlatform, currentArch, version)
62+
}
63+
}
5664
}
5765

5866
val activeConfigs = extension.getActiveConfigurations(currentPlatform, currentArch)
@@ -125,6 +133,22 @@ object ConfigurationPresets {
125133
}
126134
}
127135

136+
/**
137+
* Fault-injection configuration: identical to release but with
138+
* -D__FAULT_INJECTION__, which activates the compile-time fault-injection
139+
* layer (faultInjection.h) at the profiler's memory-access sites. Opt-in
140+
* only (see setupStandardConfigurations); never shipped in production.
141+
*/
142+
fun configureFaultInjection(
143+
config: BuildConfiguration,
144+
platform: Platform,
145+
architecture: Architecture,
146+
version: String
147+
) {
148+
configureRelease(config, platform, architecture, version)
149+
config.compilerArgs.add("-D__FAULT_INJECTION__")
150+
}
151+
128152
fun configureDebug(
129153
config: BuildConfiguration,
130154
platform: Platform,

build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestTaskBuilder.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,13 @@ class GtestTaskBuilder(
257257
// Mark unit-test builds so test-only production APIs are compiled in.
258258
args.add("-DUNIT_TEST")
259259

260+
// Mirror the opt-in fault-injection define so the fault-injection unit
261+
// test can compile the enabled path under -PenableFaultInjection. Guard
262+
// against duplicating it if the config already carries it.
263+
if (project.hasProperty("enableFaultInjection") && !args.contains("-D__FAULT_INJECTION__")) {
264+
args.add("-D__FAULT_INJECTION__")
265+
}
266+
260267
return args
261268
}
262269

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2026, Datadog, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "faultInjection.h"
18+
19+
// The whole translation unit is empty unless fault injection is enabled, so a
20+
// normal build links a no-op object file.
21+
#ifdef __FAULT_INJECTION__
22+
23+
#include "os.h" // OS::page_size
24+
#include "threadLocalData.h" // ProfiledThread::currentSignalSafe / nextFiRandom
25+
#include <atomic>
26+
#include <sys/mman.h>
27+
28+
namespace faultinj {
29+
30+
// Knuth multiplicative constant (== common.h KNUTH_MULTIPLICATIVE_CONSTANT),
31+
// used to hash the function name and to advance the global fallback PRNG.
32+
static constexpr u64 KNUTH = 0x9e3779b97f4a7c15ULL;
33+
34+
// Word-alignment mask for produced addresses.
35+
static constexpr uintptr_t ALIGN_MASK = ~(uintptr_t)(sizeof(void*) - 1);
36+
37+
// Guard region: mmapped once at init() with PROT_NONE so any access faults.
38+
// Written only by init() (off the signal path); read-only afterwards.
39+
static uintptr_t g_guard_base = 0;
40+
static uintptr_t g_guard_span = 0;
41+
static bool g_guard_ok = false;
42+
43+
// Fallback PRNG for threads with no ProfiledThread context. Relaxed atomics
44+
// keep it lock-free and async-signal-safe; a lost update on a race is harmless.
45+
static std::atomic<u64> g_fallback_rng{KNUTH};
46+
47+
void init() {
48+
// A handful of pages is plenty of distinct fault addresses; keep it small.
49+
size_t span = 16 * OS::page_size;
50+
void* p = mmap(nullptr, span, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
51+
if (p == MAP_FAILED) {
52+
g_guard_ok = false; // poisonAddress() falls back to random garbage.
53+
return;
54+
}
55+
g_guard_base = (uintptr_t)p;
56+
g_guard_span = span;
57+
g_guard_ok = true;
58+
}
59+
60+
u64 nextRandom() {
61+
ProfiledThread* t = ProfiledThread::currentSignalSafe(); // never allocates
62+
if (t != nullptr) {
63+
return t->nextFiRandom();
64+
}
65+
// Fallback: relaxed atomic xorshift64. Not perfectly serialised, but the
66+
// stream only needs to be roughly uniform for injection decisions.
67+
u64 x = g_fallback_rng.load(std::memory_order_relaxed);
68+
if (x == 0) x = 1;
69+
x ^= x << 13;
70+
x ^= x >> 7;
71+
x ^= x << 17;
72+
g_fallback_rng.store(x, std::memory_order_relaxed);
73+
return x;
74+
}
75+
76+
bool shouldFire(u64 threshold, const char* fn) {
77+
// XOR with a per-function hash is a bijection on 64 bits, so it perturbs the
78+
// stream per call site while keeping the fire probability exactly
79+
// threshold/2^64.
80+
u64 r = nextRandom() ^ ((u64)(uintptr_t)fn * KNUTH);
81+
return r < threshold;
82+
}
83+
84+
uintptr_t poisonAddress() {
85+
u64 r = nextRandom();
86+
if (g_guard_ok && (r & 1u)) {
87+
// Guaranteed-unmapped guard page — deterministic SIGSEGV.
88+
return (g_guard_base + (uintptr_t)(r % g_guard_span)) & ALIGN_MASK;
89+
}
90+
// Random non-canonical address (high bit set keeps it well above any mapped
91+
// low page) — may raise SIGSEGV or SIGBUS.
92+
return ((uintptr_t)r | (uintptr_t)0x8000000000000000ULL) & ALIGN_MASK;
93+
}
94+
95+
int32_t injectInt(int32_t v, u64 threshold, const char* fn) {
96+
if (__builtin_expect(shouldFire(threshold, fn), 0)) {
97+
return (int32_t)nextRandom();
98+
}
99+
return v;
100+
}
101+
102+
int64_t injectLong(int64_t v, u64 threshold, const char* fn) {
103+
if (__builtin_expect(shouldFire(threshold, fn), 0)) {
104+
return (int64_t)nextRandom();
105+
}
106+
return v;
107+
}
108+
109+
} // namespace faultinj
110+
111+
#endif // __FAULT_INJECTION__
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2026, Datadog, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Compile-time fault-injection layer for the profiler's memory-access sites.
18+
//
19+
// The macros below wrap a pointer or value expression at a real dereference
20+
// site (VMStructs::at, walkVM, walkFP, walkDwarf). When __FAULT_INJECTION__ is
21+
// defined, each wrapped expression, with the tier's probability, is replaced by
22+
// a deliberately bad address (so the load faults and the profiler's recovery
23+
// path — SafeAccess safefetch or walkVM's setjmp/longjmp — is exercised) or a
24+
// random int/long value. When the flag is NOT defined, every macro is a strict
25+
// identity: it expands to exactly the parenthesized original expression, with
26+
// unchanged type and value category and zero runtime cost.
27+
//
28+
// pc = SafeAccess::load(INJECT_FAULT_ADDRESS_LIKELY((void**)fp));
29+
// VMMethod* m = ((VMMethod**)INJECT_FAULT_ADDRESS_UNLIKELY(fp))[off];
30+
//
31+
// The three tiers name their firing frequency: RARE 0.01%, UNLIKELY 0.1%,
32+
// LIKELY 1%. See faultInjection.cpp for the poison-address and PRNG details.
33+
34+
#ifndef _FAULT_INJECTION_H
35+
#define _FAULT_INJECTION_H
36+
37+
#ifdef __FAULT_INJECTION__
38+
39+
#include "arch.h" // u64
40+
#include <cstdint>
41+
42+
namespace faultinj {
43+
44+
// Firing probability expressed as an xorshift64 threshold (round(p * 2^64)), so
45+
// the hot-path check is a single integer compare (rng < threshold) with no
46+
// floating point in the signal handler.
47+
constexpr u64 PROB_RARE = 1844674407370955ULL; // 1e-4 (0.01%)
48+
constexpr u64 PROB_UNLIKELY = 18446744073709552ULL; // 1e-3 (0.1%)
49+
constexpr u64 PROB_LIKELY = 184467440737095520ULL; // 1e-2 (1%)
50+
51+
// Called once at profiler startup (off the signal path) to mmap the PROT_NONE
52+
// guard region used by poisonAddress(). Safe to call before any injection.
53+
void init();
54+
55+
// Draws one per-thread (or global-fallback) PRNG value. Async-signal-safe.
56+
u64 nextRandom();
57+
58+
// Returns true with probability threshold/2^64. The function name perturbs the
59+
// draw so distinct call sites get statistically independent decisions.
60+
bool shouldFire(u64 threshold, const char* fn);
61+
62+
// A word-aligned address guaranteed to fault on access: half the time a pointer
63+
// into the mmap'd PROT_NONE guard region (deterministic SIGSEGV), half the time
64+
// a random non-canonical address (SIGSEGV or SIGBUS). Arithmetic-only, no
65+
// syscall — safe on the signal-handler hot path.
66+
uintptr_t poisonAddress();
67+
68+
// Returns ptr unchanged, or a poison address (cast to T) when the tier fires.
69+
// Templated so the wrapped expression's static type (void**, const char*,
70+
// uintptr_t, ...) is preserved exactly.
71+
template <typename T>
72+
inline T injectAddress(T ptr, u64 threshold, const char* fn) {
73+
if (__builtin_expect(shouldFire(threshold, fn), 0)) {
74+
// C-style cast intentionally: converts the numeric poison address to any
75+
// pointer type or to uintptr_t. This code only ever compiles under the flag.
76+
return (T)poisonAddress();
77+
}
78+
return ptr;
79+
}
80+
81+
// Returns v unchanged, or a pseudo-random value when the tier fires.
82+
int32_t injectInt(int32_t v, u64 threshold, const char* fn);
83+
int64_t injectLong(int64_t v, u64 threshold, const char* fn);
84+
85+
} // namespace faultinj
86+
87+
#define INJECT_FAULT_ADDRESS_RARE(ptr) \
88+
::faultinj::injectAddress((ptr), ::faultinj::PROB_RARE, __func__)
89+
#define INJECT_FAULT_ADDRESS_UNLIKELY(ptr) \
90+
::faultinj::injectAddress((ptr), ::faultinj::PROB_UNLIKELY, __func__)
91+
#define INJECT_FAULT_ADDRESS_LIKELY(ptr) \
92+
::faultinj::injectAddress((ptr), ::faultinj::PROB_LIKELY, __func__)
93+
94+
#define INJECT_FAULT_INT_RARE(v) \
95+
::faultinj::injectInt((v), ::faultinj::PROB_RARE, __func__)
96+
#define INJECT_FAULT_INT_UNLIKELY(v) \
97+
::faultinj::injectInt((v), ::faultinj::PROB_UNLIKELY, __func__)
98+
#define INJECT_FAULT_INT_LIKELY(v) \
99+
::faultinj::injectInt((v), ::faultinj::PROB_LIKELY, __func__)
100+
101+
#define INJECT_FAULT_LONG_RARE(v) \
102+
::faultinj::injectLong((v), ::faultinj::PROB_RARE, __func__)
103+
#define INJECT_FAULT_LONG_UNLIKELY(v) \
104+
::faultinj::injectLong((v), ::faultinj::PROB_UNLIKELY, __func__)
105+
#define INJECT_FAULT_LONG_LIKELY(v) \
106+
::faultinj::injectLong((v), ::faultinj::PROB_LIKELY, __func__)
107+
108+
#else // __FAULT_INJECTION__ not defined — strict identity, zero cost.
109+
110+
#define INJECT_FAULT_ADDRESS_RARE(ptr) (ptr)
111+
#define INJECT_FAULT_ADDRESS_UNLIKELY(ptr) (ptr)
112+
#define INJECT_FAULT_ADDRESS_LIKELY(ptr) (ptr)
113+
114+
#define INJECT_FAULT_INT_RARE(v) (v)
115+
#define INJECT_FAULT_INT_UNLIKELY(v) (v)
116+
#define INJECT_FAULT_INT_LIKELY(v) (v)
117+
118+
#define INJECT_FAULT_LONG_RARE(v) (v)
119+
#define INJECT_FAULT_LONG_UNLIKELY(v) (v)
120+
#define INJECT_FAULT_LONG_LIKELY(v) (v)
121+
122+
#endif // __FAULT_INJECTION__
123+
124+
#endif // _FAULT_INJECTION_H

ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <climits>
88
#include <cstdlib>
99
#include "asyncSampleMutex.h"
10+
#include "faultInjection.h"
1011
#include "frames.h"
1112
#include "guards.h"
1213
#include "hotspot/hotspotSupport.h"
@@ -370,8 +371,8 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
370371
// entry_fp has been range-checked by isValidFP above; any remaining
371372
// SIGSEGV from a stale/concurrently-freed pointer is caught by the
372373
// setjmp crash protection in walkVM (checkFault -> longjmp).
373-
uintptr_t carrier_fp = *(uintptr_t*)entry_fp;
374-
const void* carrier_pc = ((const void**)entry_fp)[FRAME_PC_SLOT];
374+
uintptr_t carrier_fp = *(uintptr_t*)INJECT_FAULT_ADDRESS_UNLIKELY(entry_fp);
375+
const void* carrier_pc = ((const void**)INJECT_FAULT_ADDRESS_UNLIKELY(entry_fp))[FRAME_PC_SLOT];
375376
uintptr_t carrier_sp = entry_fp + (FRAME_PC_SLOT + 1) * sizeof(void*);
376377
if (!StackWalkValidation::isValidFP(carrier_fp) ||
377378
StackWalkValidation::inDeadZone(carrier_pc) ||
@@ -496,7 +497,7 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
496497

497498
bool is_plausible_interpreter_frame = StackWalkValidation::isPlausibleInterpreterFrame(fp, sp, bcp_offset);
498499
if (is_plausible_interpreter_frame) {
499-
VMMethod* method = ((VMMethod**)fp)[InterpreterFrame::method_offset];
500+
VMMethod* method = ((VMMethod**)INJECT_FAULT_ADDRESS_UNLIKELY(fp))[InterpreterFrame::method_offset];
500501
jmethodID method_id = getMethodId(method);
501502
if (method_id != JMETHODID_NOT_WALKABLE) {
502503
Counters::increment(WALKVM_JAVA_FRAME_OK);
@@ -506,7 +507,7 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
506507
fillFrame(frames[depth++], FRAME_INTERPRETED, bci, method_id, method);
507508
sp = ((uintptr_t*)fp)[InterpreterFrame::sender_sp_offset];
508509
pc = stripPointer(((void**)fp)[FRAME_PC_SLOT]);
509-
fp = *(uintptr_t*)fp;
510+
fp = *(uintptr_t*)INJECT_FAULT_ADDRESS_UNLIKELY(fp);
510511
continue;
511512
}
512513
}
@@ -518,7 +519,7 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
518519
Counters::increment(WALKVM_JAVA_FRAME_OK);
519520
fillFrame(frames[depth++], FRAME_INTERPRETED, 0, method_id, method);
520521
if (is_plausible_interpreter_frame) {
521-
pc = stripPointer(((void**)fp)[FRAME_PC_SLOT]);
522+
pc = stripPointer(((void**)INJECT_FAULT_ADDRESS_UNLIKELY(fp))[FRAME_PC_SLOT]);
522523
sp = frame.senderSP();
523524
fp = *(uintptr_t*)fp;
524525
} else {
@@ -595,7 +596,7 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
595596
break;
596597
}
597598

598-
fp = ((uintptr_t*)sp)[-FRAME_PC_SLOT - 1];
599+
fp = ((uintptr_t*)INJECT_FAULT_ADDRESS_UNLIKELY(sp))[-FRAME_PC_SLOT - 1];
599600
pc = ((const void**)sp)[-FRAME_PC_SLOT];
600601
continue;
601602
} else if (frame.unwindPrologue(nm, (uintptr_t&)pc, sp, fp)) {
@@ -737,7 +738,7 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
737738
// In HotSpot, lastJavaFP is non-zero only for interpreter frames;
738739
// compiled frames record FP=0 in the anchor.
739740
if (StackWalkValidation::isPlausibleInterpreterFrame(recovery_fp, recovery_sp, bcp_offset)) {
740-
VMMethod* method = ((VMMethod**)recovery_fp)[InterpreterFrame::method_offset];
741+
VMMethod* method = ((VMMethod**)INJECT_FAULT_ADDRESS_UNLIKELY(recovery_fp))[InterpreterFrame::method_offset];
741742
jmethodID method_id = getMethodId(method);
742743
if (method_id != JMETHODID_NOT_WALKABLE) {
743744
anchor = NULL;

ddprof-lib/src/main/cpp/hotspot/vmStructs.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <type_traits>
1515
#include "codeCache.h"
1616
#include "counters.h"
17+
#include "faultInjection.h"
1718
#include "jvmThread.h"
1819
#include "safeAccess.h"
1920
#include "threadState.h"
@@ -451,13 +452,14 @@ class VMStructs {
451452
const char* at(int offset) {
452453
const char* ptr = (const char*)this + offset;
453454
assert(crashProtectionActive() || SafeAccess::isReadable(ptr));
454-
return ptr;
455+
// Poison only the returned pointer; the assert above sees the real ptr.
456+
return INJECT_FAULT_ADDRESS_RARE(ptr);
455457
}
456458

457459
const char* at(int offset) const {
458460
const char* ptr = (const char*)this + offset;
459461
assert(crashProtectionActive() || SafeAccess::isReadable(ptr));
460-
return ptr;
462+
return INJECT_FAULT_ADDRESS_RARE(ptr);
461463
}
462464

463465
static bool goodPtr(const void* ptr) {

ddprof-lib/src/main/cpp/profiler.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "ctimer.h"
1717
#include "signalInflight.h"
1818
#include "dwarf.h"
19+
#include "faultInjection.h"
1920
#include "flightRecorder.h"
2021
#include "itimer.h"
2122
#include "hotspot/vmStructs.inline.h"
@@ -998,6 +999,11 @@ void Profiler::setupSignalHandlers() {
998999
// Patch sigaction GOT in libraries with broken signal handlers (already loaded)
9991000
LibraryPatcher::patch_sigaction();
10001001
}
1002+
#ifdef __FAULT_INJECTION__
1003+
// Reserve the PROT_NONE guard region used to poison memory-access sites.
1004+
// Done here (off the signal path) once handlers are installed.
1005+
faultinj::init();
1006+
#endif
10011007
}
10021008
}
10031009

0 commit comments

Comments
 (0)