Skip to content

Commit 980bc8d

Browse files
authored
fix(profiler): guard JVMTI sample path against null calltrace buffer (#536)
1 parent ef13aa2 commit 980bc8d

10 files changed

Lines changed: 134 additions & 111 deletions

ddprof-lib/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ gtest {
5050
"$javaHome/include/$platformInclude",
5151
project(":malloc-shim").file("src/main/public"),
5252
)
53+
54+
failFast.set(true)
5355
}
5456

5557
// Java configuration - using sourceCompatibility (not --release 8)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1926,6 +1926,7 @@ void FlightRecorder::recordEventDelegated(int lock_index, int tid,
19261926
// Delegation is only wired for CPU/wall samples in v1.
19271927
break;
19281928
}
1929+
rec->flushIfNeeded(buf);
19291930
rec->addThread(lock_index, tid);
19301931
}
19311932
}

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

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -458,11 +458,20 @@ u64 Profiler::recordJVMTISample(u64 counter, int tid, jthread thread, jint event
458458
}
459459
u64 call_trace_id = 0;
460460
if (!_omit_stacktraces) {
461+
// Defensive: the buffer slot can be observed as nullptr in pathological
462+
// start sequences (e.g. a calloc failure path in Profiler::start before
463+
// engines are enabled). Drop the sample rather than dereferencing.
464+
CallTraceBuffer *buf = _calltrace_buffer[lock_index];
465+
if (buf == nullptr) {
466+
atomicIncRelaxed(_failures[-ticks_skipped]);
467+
_locks[lock_index].unlock();
468+
return 0;
469+
}
461470
#ifdef COUNTERS
462471
u64 startTime = TSC::ticks();
463472
#endif // COUNTERS
464-
ASGCT_CallFrame *frames = _calltrace_buffer[lock_index]->_asgct_frames;
465-
jvmtiFrameInfo *jvmti_frames = _calltrace_buffer[lock_index]->_jvmti_frames;
473+
ASGCT_CallFrame *frames = buf->_asgct_frames;
474+
jvmtiFrameInfo *jvmti_frames = buf->_jvmti_frames;
466475

467476
int num_frames = 0;
468477

@@ -676,24 +685,28 @@ void Profiler::recordExternalSample(u64 weight, int tid, int num_frames,
676685
CriticalSection cs;
677686
atomicIncRelaxed(_total_samples);
678687

679-
// Convert ASGCT_CallFrame to ASGCT_CallFrame
680-
// External samplers (like ObjectSampler) provide standard frames only
681688
u32 lock_index = getLockIndex(tid);
682-
ASGCT_CallFrame *extended_frames = _calltrace_buffer[lock_index]->_asgct_frames;
683-
for (int i = 0; i < num_frames; i++) {
684-
extended_frames[i] = frames[i];
685-
}
686-
687-
u64 call_trace_id =
688-
_call_trace_storage.put(num_frames, extended_frames, truncated, weight);
689689
if (!_locks[lock_index].tryLock() &&
690690
!_locks[lock_index = (lock_index + 1) % CONCURRENCY_LEVEL].tryLock() &&
691691
!_locks[lock_index = (lock_index + 2) % CONCURRENCY_LEVEL].tryLock()) {
692-
// Too many concurrent signals already
693692
atomicIncRelaxed(_failures[-ticks_skipped]);
694693
return;
695694
}
696695

696+
CallTraceBuffer *buf = _calltrace_buffer[lock_index];
697+
if (buf == nullptr) {
698+
atomicIncRelaxed(_failures[-ticks_skipped]);
699+
_locks[lock_index].unlock();
700+
return;
701+
}
702+
// External samplers (like ObjectSampler) provide standard frames only
703+
ASGCT_CallFrame *extended_frames = buf->_asgct_frames;
704+
for (int i = 0; i < num_frames; i++) {
705+
extended_frames[i] = frames[i];
706+
}
707+
708+
u64 call_trace_id =
709+
_call_trace_storage.put(num_frames, extended_frames, truncated, weight);
697710
_jfr.recordEvent(lock_index, tid, call_trace_id, event_type, event);
698711

699712
_locks[lock_index].unlock();
@@ -1166,13 +1179,23 @@ Error Profiler::start(Arguments &args, bool reset) {
11661179
size_t nelem = _max_stack_depth + RESERVED_FRAMES;
11671180

11681181
for (int i = 0; i < CONCURRENCY_LEVEL; i++) {
1169-
free(_calltrace_buffer[i]);
1170-
_calltrace_buffer[i] = (CallTraceBuffer*)calloc(nelem, sizeof(CallTraceBuffer));
1171-
if (_calltrace_buffer[i] == NULL) {
1182+
// Allocate the replacement before touching the slot so a calloc failure
1183+
// does not leave the slot pointing at freed memory.
1184+
CallTraceBuffer *fresh =
1185+
(CallTraceBuffer*)calloc(nelem, sizeof(CallTraceBuffer));
1186+
if (fresh == NULL) {
11721187
_max_stack_depth = 0;
11731188
return Error("Not enough memory to allocate stack trace buffers (try "
11741189
"smaller jstackdepth)");
11751190
}
1191+
// Swap under the per-shard lock: all readers (recordJVMTISample,
1192+
// recordExternalSample) acquire this lock via tryLock before reading
1193+
// _calltrace_buffer, so no reader can observe a freed pointer mid-replacement.
1194+
_locks[i].lock();
1195+
CallTraceBuffer *prev = _calltrace_buffer[i];
1196+
_calltrace_buffer[i] = fresh;
1197+
_locks[i].unlock();
1198+
free(prev);
11761199
}
11771200
}
11781201

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,11 @@ class alignas(alignof(SpinLock)) Profiler {
9797
void *_timer_id;
9898

9999
volatile u64 _total_samples;
100+
// On a separate cache line: incremented from every signal handler via
101+
// recordSampleDelegated; must not share a line with _failures (written by
102+
// ASGCT paths) or _total_samples (written by every recording path).
100103
alignas(DEFAULT_CACHE_LINE_SIZE) volatile u64 _sample_seq;
101-
u64 _failures[ASGCT_FAILURE_TYPES];
104+
alignas(DEFAULT_CACHE_LINE_SIZE) u64 _failures[ASGCT_FAILURE_TYPES];
102105

103106
SpinLock _class_map_lock;
104107
SpinLock _locks[CONCURRENCY_LEVEL];

ddprof-lib/src/main/cpp/vmEntry.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,10 @@ class VM {
129129
static bool _is_adaptive_gc_boundary_flag_set;
130130

131131
// HotSpot JFR async stack-trace extension (optional, JDK 27+).
132-
// Null until InitializeRequestStackTrace succeeds; published with RELEASE.
132+
// _request_stack_trace is atomic (RELEASE/ACQUIRE) because canRequestStackTrace()
133+
// is called from signal handlers; _init_request_stack_trace is plain because it
134+
// is only ever read by initializeRequestStackTrace(), called once from the same
135+
// init thread before any signal handlers are installed.
133136
static jvmtiExtensionFunction _request_stack_trace;
134137
static jvmtiExtensionFunction _init_request_stack_trace;
135138

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

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
std::atomic<bool> BaseWallClock::_enabled{false};
2929

30-
bool WallClockASGCT::inSyscall(void *ucontext) {
30+
bool BaseWallClock::inSyscall(void *ucontext) {
3131
StackFrame frame(ucontext);
3232
uintptr_t pc = frame.pc();
3333

@@ -226,27 +226,6 @@ void WallClockASGCT::timerLoop() {
226226
// instead of invoking ASGCT. Used only when VM::canRequestStackTrace() is true
227227
// and the profiler has opted into jvmtistacks.
228228

229-
bool WallClockJvmti::inSyscall(void *ucontext) {
230-
StackFrame frame(ucontext);
231-
uintptr_t pc = frame.pc();
232-
233-
if (StackFrame::isSyscall((instruction_t *)pc)) {
234-
return true;
235-
}
236-
237-
uintptr_t prev_pc = pc - SYSCALL_SIZE;
238-
if ((pc & 0xfff) >= SYSCALL_SIZE ||
239-
Libraries::instance()->findLibraryByAddress((instruction_t *)prev_pc) !=
240-
NULL) {
241-
if (StackFrame::isSyscall((instruction_t *)prev_pc) &&
242-
frame.checkInterruptedSyscall()) {
243-
return true;
244-
}
245-
}
246-
247-
return false;
248-
}
249-
250229
void WallClockJvmti::sharedSignalHandler(int signo, siginfo_t *siginfo,
251230
void *ucontext) {
252231
WallClockJvmti *engine =
@@ -327,6 +306,8 @@ void WallClockJvmti::timerLoop() {
327306
threads_already_exited++;
328307
} else if (errno == EPERM) {
329308
permission_denied++;
309+
} else if (errno == EAGAIN) {
310+
// Signal queue limit (RLIMIT_SIGPENDING) reached — count as missed.
330311
} else {
331312
Log::debug("unexpected error %s", strerror(errno));
332313
}

ddprof-lib/src/main/cpp/wallClock.h

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class BaseWallClock : public Engine {
4040
}
4141

4242
bool isEnabled() const;
43+
static bool inSyscall(void* ucontext);
4344

4445
template <typename ThreadType, typename CollectThreadsFunc, typename SampleThreadsFunc, typename CleanThreadFunc>
4546
void timerLoopCommon(CollectThreadsFunc collectThreads, SampleThreadsFunc sampleThreads, CleanThreadFunc cleanThreads, int reservoirSize, u64 interval) {
@@ -142,8 +143,6 @@ class WallClockASGCT : public BaseWallClock {
142143
private:
143144
bool _collapsing;
144145

145-
static bool inSyscall(void* ucontext);
146-
147146
static void sharedSignalHandler(int signo, siginfo_t* siginfo, void* ucontext);
148147
void signalHandler(int signo, siginfo_t* siginfo, void* ucontext, u64 last_sample);
149148

@@ -163,8 +162,6 @@ class WallClockASGCT : public BaseWallClock {
163162
// VM::canRequestStackTrace().
164163
class WallClockJvmti : public BaseWallClock {
165164
private:
166-
static bool inSyscall(void* ucontext);
167-
168165
static void sharedSignalHandler(int signo, siginfo_t* siginfo, void* ucontext);
169166
void signalHandler(int signo, siginfo_t* siginfo, void* ucontext, u64 last_sample);
170167

ddprof-lib/src/test/cpp/objectSampler_ut.cpp

Lines changed: 31 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,17 @@
66
#include <cstddef>
77
#include <cstring>
88
#include "../../main/cpp/objectSampler.h"
9+
#include "../../main/cpp/gtest_crash_handler.h"
910
#include "vmEntry.h"
1011

12+
static constexpr char OBJECT_SAMPLER_TEST_NAME[] = "ObjectSamplerTest";
13+
class ObjectSamplerGlobalSetup {
14+
public:
15+
ObjectSamplerGlobalSetup() { installGtestCrashHandler<OBJECT_SAMPLER_TEST_NAME>(); }
16+
~ObjectSamplerGlobalSetup() { restoreDefaultSignalHandlers(); }
17+
};
18+
static ObjectSamplerGlobalSetup object_sampler_global_setup;
19+
1120
// ---------------------------------------------------------------------------
1221
// ObjectSamplerTestAccessor — friend of ObjectSampler, exposes internals
1322
// needed by the regression tests.
@@ -46,10 +55,7 @@ class ObjectSamplerDeallocateTest : public ::testing::Test {
4655
_jvmtiEnv mock_env{};
4756
int deallocate_calls = 0;
4857

49-
// Active fixture pointer used by the C-style mock callbacks to reach
50-
// per-instance state. Reset in SetUp/TearDown so the mocks never see a
51-
// stale fixture even if a previous test crashed mid-run.
52-
static thread_local ObjectSamplerDeallocateTest *active_fixture;
58+
static ObjectSamplerDeallocateTest *active_fixture;
5359

5460
void SetUp() override {
5561
deallocate_calls = 0;
@@ -71,22 +77,27 @@ class ObjectSamplerDeallocateTest : public ::testing::Test {
7177
tbl.GetClassSignature = fn;
7278
}
7379

74-
// Mock Deallocate increments the per-fixture counter; it never frees the
75-
// pointer because the mock signature buffer is statically allocated.
80+
void runAndExpect(
81+
jvmtiError(JNICALL *mock)(jvmtiEnv *, jclass, char **, char **),
82+
bool active, int expected_calls) {
83+
setMockGetClassSignature(mock);
84+
ObjectSampler *s = ObjectSampler::instance();
85+
ObjectSamplerTestAccessor::setActive(s, active);
86+
ObjectSamplerTestAccessor::callRecordAllocation(
87+
s, &mock_env, nullptr, nullptr, BCI_ALLOC, nullptr, nullptr, 1024);
88+
EXPECT_EQ(deallocate_calls, expected_calls);
89+
}
90+
7691
static jvmtiError JNICALL mock_Deallocate(jvmtiEnv * /*env*/,
7792
unsigned char * /*mem*/) {
78-
if (active_fixture) {
79-
++active_fixture->deallocate_calls;
80-
}
93+
++active_fixture->deallocate_calls;
8194
return JVMTI_ERROR_NONE;
8295
}
8396
};
8497

85-
thread_local ObjectSamplerDeallocateTest *
98+
ObjectSamplerDeallocateTest *
8699
ObjectSamplerDeallocateTest::active_fixture = nullptr;
87100

88-
// GetClassSignature mock: returns JVMTI_ERROR_NONE and writes
89-
// g_mock_class_name into *signature_ptr.
90101
static jvmtiError JNICALL mock_GetClassSignature_success(
91102
jvmtiEnv * /*env*/, jclass /*klass*/,
92103
char **signature_ptr, char ** /*generic_ptr*/) {
@@ -96,28 +107,24 @@ static jvmtiError JNICALL mock_GetClassSignature_success(
96107
return JVMTI_ERROR_NONE;
97108
}
98109

99-
// GetClassSignature mock: returns an error AND writes a non-NULL sentinel
100-
// into *signature_ptr (the UAF scenario we are guarding against).
110+
// Returns error but writes a non-NULL sentinel into *signature_ptr — the UAF
111+
// scenario the fix guards against (Deallocate must not be called on error).
101112
static jvmtiError JNICALL mock_GetClassSignature_error_with_sentinel(
102113
jvmtiEnv * /*env*/, jclass /*klass*/,
103114
char **signature_ptr, char ** /*generic_ptr*/) {
104115
if (signature_ptr) {
105-
*signature_ptr = g_mock_class_name; // sentinel: non-NULL despite error
116+
*signature_ptr = g_mock_class_name;
106117
}
107118
return JVMTI_ERROR_INVALID_CLASS;
108119
}
109120

110-
// GetClassSignature mock: returns an error and leaves *signature_ptr at NULL.
111121
static jvmtiError JNICALL mock_GetClassSignature_error_null(
112122
jvmtiEnv * /*env*/, jclass /*klass*/,
113123
char **signature_ptr, char ** /*generic_ptr*/) {
114-
// Leave *signature_ptr unchanged (NULL as initialised by recordAllocation).
115124
(void)signature_ptr;
116125
return JVMTI_ERROR_INVALID_CLASS;
117126
}
118127

119-
// GetClassSignature mock: returns JVMTI_ERROR_NONE but writes NULL into
120-
// *signature_ptr — a misbehaving JVMTI impl.
121128
static jvmtiError JNICALL mock_GetClassSignature_success_null_name(
122129
jvmtiEnv * /*env*/, jclass /*klass*/,
123130
char **signature_ptr, char ** /*generic_ptr*/) {
@@ -208,74 +215,34 @@ TEST(ObjectSamplerTest, NormalizePassesThroughObjectArray) {
208215
EXPECT_EQ(out_len, strlen("[Ljava/lang/String;"));
209216
}
210217

211-
// ---------------------------------------------------------------------------
212218
// T-01: GetClassSignature returns error with non-NULL sentinel in *signature_ptr.
213219
// Deallocate MUST NOT be called.
214-
// ---------------------------------------------------------------------------
215220
TEST_F(ObjectSamplerDeallocateTest, DeallocateNotCalledOnErrorWithNonNullSentinel) {
216-
setMockGetClassSignature(mock_GetClassSignature_error_with_sentinel);
217-
ObjectSampler *s = ObjectSampler::instance();
218-
ObjectSamplerTestAccessor::setActive(s, true);
219-
ObjectSamplerTestAccessor::callRecordAllocation(
220-
s, &mock_env, nullptr, nullptr, BCI_ALLOC,
221-
nullptr, nullptr, 1024);
222-
EXPECT_EQ(deallocate_calls, 0);
221+
runAndExpect(mock_GetClassSignature_error_with_sentinel, true, 0);
223222
}
224223

225-
// ---------------------------------------------------------------------------
226224
// T-02: GetClassSignature succeeds with a valid class name.
227225
// Deallocate IS called exactly once (on the success path).
228226
// Note: lookupClass returns -1 because the class map is empty, so the
229227
// method returns without recording — that is the expected behaviour.
230-
// ---------------------------------------------------------------------------
231228
TEST_F(ObjectSamplerDeallocateTest, DeallocateCalledOnceOnGetClassSignatureSuccess) {
232-
setMockGetClassSignature(mock_GetClassSignature_success);
233-
ObjectSampler *s = ObjectSampler::instance();
234-
ObjectSamplerTestAccessor::setActive(s, true);
235-
ObjectSamplerTestAccessor::callRecordAllocation(
236-
s, &mock_env, nullptr, nullptr, BCI_ALLOC,
237-
nullptr, nullptr, 1024);
238-
EXPECT_EQ(deallocate_calls, 1);
229+
runAndExpect(mock_GetClassSignature_success, true, 1);
239230
}
240231

241-
// ---------------------------------------------------------------------------
242232
// T-03: GetClassSignature fails and leaves class_name at NULL.
243233
// Deallocate MUST NOT be called.
244-
// ---------------------------------------------------------------------------
245234
TEST_F(ObjectSamplerDeallocateTest, DeallocateNotCalledOnErrorWithNullName) {
246-
setMockGetClassSignature(mock_GetClassSignature_error_null);
247-
ObjectSampler *s = ObjectSampler::instance();
248-
ObjectSamplerTestAccessor::setActive(s, true);
249-
ObjectSamplerTestAccessor::callRecordAllocation(
250-
s, &mock_env, nullptr, nullptr, BCI_ALLOC,
251-
nullptr, nullptr, 1024);
252-
EXPECT_EQ(deallocate_calls, 0);
235+
runAndExpect(mock_GetClassSignature_error_null, true, 0);
253236
}
254237

255-
// ---------------------------------------------------------------------------
256238
// T-04: GetClassSignature succeeds but writes NULL into *signature_ptr.
257239
// Deallocate MUST NOT be called (the NULL guard in the condition fires).
258-
// ---------------------------------------------------------------------------
259240
TEST_F(ObjectSamplerDeallocateTest, DeallocateNotCalledWhenSuccessButNullName) {
260-
setMockGetClassSignature(mock_GetClassSignature_success_null_name);
261-
ObjectSampler *s = ObjectSampler::instance();
262-
ObjectSamplerTestAccessor::setActive(s, true);
263-
ObjectSamplerTestAccessor::callRecordAllocation(
264-
s, &mock_env, nullptr, nullptr, BCI_ALLOC,
265-
nullptr, nullptr, 1024);
266-
EXPECT_EQ(deallocate_calls, 0);
241+
runAndExpect(mock_GetClassSignature_success_null_name, true, 0);
267242
}
268243

269-
// ---------------------------------------------------------------------------
270244
// T-05: _active is false — recordAllocation returns immediately.
271245
// Deallocate MUST NOT be called.
272-
// ---------------------------------------------------------------------------
273246
TEST_F(ObjectSamplerDeallocateTest, DeallocateNotCalledWhenNotActive) {
274-
setMockGetClassSignature(mock_GetClassSignature_success);
275-
ObjectSampler *s = ObjectSampler::instance();
276-
ObjectSamplerTestAccessor::setActive(s, false);
277-
ObjectSamplerTestAccessor::callRecordAllocation(
278-
s, &mock_env, nullptr, nullptr, BCI_ALLOC,
279-
nullptr, nullptr, 1024);
280-
EXPECT_EQ(deallocate_calls, 0);
247+
runAndExpect(mock_GetClassSignature_success, false, 0);
281248
}

0 commit comments

Comments
 (0)