Skip to content

Commit a8551a2

Browse files
authored
[PROF-15201] fix(profiler): close two TOCTOU races between SIGPROF handler and JFR lifecycle (#614)
fix(profiler): drain in-flight signal handlers before JFR teardown Close two TOCTOU races between profiling signal handlers and the JFR lifecycle that could cause use-after-free or hangs during recording-cycle rotation. - _enabled: unify __ATOMIC_RELEASE/ACQUIRE across CTimer, CTimerJvmti, ITimer, ITimerJvmti, PerfEvents, WallClockASGCT, WallClockJvmti so arm64 handlers cannot observe stale _enabled=true after disableEngines() has returned. - Ordering: Profiler::stop() now runs disableEngines() → drain → per-engine stop() → _jfr.stop(); Profiler::start() enables wall before its pthread starts and CPU only after _jfr.start() completes. - Drain: new SignalInflight module + InflightGuard RAII counter on a dedicated cache line; every JFR-writing signal handler participates. drain() spins to 0 with a 200 ms deadline; on timeout, stop() returns Error and leaves _state == RUNNING so retry is safe (no non-idempotent engine teardown has run). co-authored by claude Great reviews from both @rkennke, @jbachorik
1 parent 8c8dc6a commit a8551a2

12 files changed

Lines changed: 237 additions & 25 deletions

File tree

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@
2525
#include <signal.h>
2626

2727
class CTimer : public Engine {
28+
private:
29+
// cppcheck-suppress unusedPrivateFunction
30+
static void signalHandler(int signo, siginfo_t *siginfo, void *ucontext);
31+
2832
protected:
29-
// This is accessed from signal handlers, so must be async-signal-safe
33+
// Accessed from signal handlers (including CTimerJvmti subclass), so must
34+
// be async-signal-safe. Mutated via enableEvents().
3035
static bool _enabled;
36+
3137
static long _interval;
3238
static CStack _cstack;
3339
static int _signal;
@@ -38,10 +44,6 @@ class CTimer : public Engine {
3844
int registerThread(int tid);
3945
void unregisterThread(int tid);
4046

41-
private:
42-
// cppcheck-suppress unusedPrivateFunction
43-
static void signalHandler(int signo, siginfo_t *siginfo, void *ucontext);
44-
4547
public:
4648
const char *units() { return "ns"; }
4749

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "counters.h"
2121
#include "guards.h"
2222
#include "ctimer.h"
23+
#include "signalInflight.h"
2324
#include "debugSupport.h"
2425
#include "jvmThread.h"
2526
#include "libraries.h"
@@ -180,6 +181,8 @@ void CTimer::stop() {
180181
for (int i = 0; i < _max_timers; i++) {
181182
unregisterThread(i);
182183
}
184+
// Note: SignalInflight::drain() is called by Profiler::stop() after
185+
// disableEngines(); see signalInflight.h.
183186
}
184187

185188
Error CTimerJvmti::check(Arguments &args) {
@@ -210,6 +213,8 @@ void CTimerJvmti::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
210213
}
211214
Counters::increment(CTIMER_SIGNAL_OWN);
212215

216+
InflightGuard inflight;
217+
213218
CriticalSection cs;
214219
if (!cs.entered()) {
215220
return;
@@ -261,6 +266,8 @@ void CTimer::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
261266
}
262267
Counters::increment(CTIMER_SIGNAL_OWN);
263268

269+
InflightGuard inflight;
270+
264271
// Atomically try to enter critical section - prevents all reentrancy races
265272
CriticalSection cs;
266273
if (!cs.entered()) {
@@ -270,8 +277,9 @@ void CTimer::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
270277
int saved_errno = errno;
271278
// we want to ensure memory order because of the possibility the instance gets
272279
// cleared
273-
if (!__atomic_load_n(&_enabled, __ATOMIC_ACQUIRE))
280+
if (!__atomic_load_n(&_enabled, __ATOMIC_ACQUIRE)) {
274281
return;
282+
}
275283
int tid = 0;
276284
ProfiledThread *current = ProfiledThread::currentSignalSafe();
277285
assert(current == nullptr || !current->isDeepCrashHandler());

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@
2020
#include "jvmThread.h"
2121
#include "os.h"
2222
#include "profiler.h"
23+
#include "signalInflight.h"
2324
#include "stackWalker.h"
2425
#include "thread.h"
2526
#include "threadState.inline.h"
2627
#include "guards.h"
2728
#include <sys/time.h>
2829

29-
volatile bool ITimer::_enabled = false;
30+
bool ITimer::_enabled = false;
3031
long ITimer::_interval;
3132
CStack ITimer::_cstack;
3233

@@ -38,7 +39,8 @@ void ITimer::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
3839
// is therefore vulnerable to the foreign-SIGPROF deadlock scenario this
3940
// feature addresses. Use CTimer (the default) when signal-origin
4041
// validation is required.
41-
if (!_enabled)
42+
InflightGuard inflight;
43+
if (!__atomic_load_n(&_enabled, __ATOMIC_ACQUIRE))
4244
return;
4345

4446
// Atomically try to enter critical section - prevents all reentrancy races
@@ -99,11 +101,12 @@ void ITimer::stop() {
99101
setitimer(ITIMER_PROF, &tv, NULL);
100102
}
101103

102-
volatile bool ITimerJvmti::_enabled = false;
104+
bool ITimerJvmti::_enabled = false;
103105
long ITimerJvmti::_interval = 0;
104106

105107
void ITimerJvmti::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
106108
SIGNAL_HANDLER_GUARD();
109+
InflightGuard inflight;
107110
CriticalSection cs;
108111
if (!cs.entered()) {
109112
return;

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

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

2323
class ITimer : public Engine {
2424
private:
25-
static volatile bool _enabled;
25+
static bool _enabled;
2626
static long _interval;
2727
static CStack _cstack;
2828

@@ -39,7 +39,9 @@ class ITimer : public Engine {
3939
Error start(Arguments &args);
4040
void stop();
4141

42-
inline void enableEvents(bool enabled) { _enabled = enabled; }
42+
inline void enableEvents(bool enabled) {
43+
__atomic_store_n(&_enabled, enabled, __ATOMIC_RELEASE);
44+
}
4345
};
4446

4547
// CPU-time engine identical to ITimer in its timer mechanism (process-wide
@@ -51,7 +53,7 @@ class ITimer : public Engine {
5153
// stack walking rather than relying on the signal-frame PC.
5254
class ITimerJvmti : public Engine {
5355
private:
54-
static volatile bool _enabled;
56+
static bool _enabled;
5557
static long _interval;
5658

5759
static void signalHandler(int signo, siginfo_t *siginfo, void *ucontext);

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class StackContext;
3030

3131
class PerfEvents : public Engine {
3232
private:
33-
static volatile bool _enabled;
33+
static bool _enabled;
3434
static int _max_events;
3535
static PerfEvent *_events;
3636
static PerfEventType *_event_type;
@@ -62,7 +62,9 @@ class PerfEvents : public Engine {
6262

6363
static const char *getEventName(int event_id);
6464

65-
inline void enableEvents(bool enabled) { _enabled = enabled; }
65+
inline void enableEvents(bool enabled) {
66+
__atomic_store_n(&_enabled, enabled, __ATOMIC_RELEASE);
67+
}
6668
};
6769

6870
#else

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "os.h"
3030
#include "perfEvents.h"
3131
#include "profiler.h"
32+
#include "signalInflight.h"
3233
#include "spinLock.h"
3334
#include "stackFrame.h"
3435
#include "stackWalker.h"
@@ -560,7 +561,7 @@ class PerfEvent : public SpinLock {
560561
friend class PerfEvents;
561562
};
562563

563-
volatile bool PerfEvents::_enabled = false;
564+
bool PerfEvents::_enabled = false;
564565
int PerfEvents::_max_events = -1;
565566
PerfEvent *PerfEvents::_events = NULL;
566567
PerfEventType *PerfEvents::_event_type = NULL;
@@ -735,6 +736,7 @@ void PerfEvents::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
735736
// Looks like an external signal; don't treat as a profiling event
736737
return;
737738
}
739+
InflightGuard inflight;
738740
// Atomically try to enter critical section - prevents all reentrancy races
739741
CriticalSection cs;
740742
if (!cs.entered()) {
@@ -745,7 +747,7 @@ void PerfEvents::signalHandler(int signo, siginfo_t *siginfo, void *ucontext) {
745747
current->noteCPUSample(Profiler::instance()->recordingEpoch());
746748
}
747749
int tid = current != NULL ? current->tid() : OS::threadId();
748-
if (_enabled) {
750+
if (__atomic_load_n(&_enabled, __ATOMIC_ACQUIRE)) {
749751
Shims::instance().setSighandlerTid(tid);
750752

751753
u64 counter = readCounter(siginfo, ucontext);

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

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "common.h"
1515
#include "counters.h"
1616
#include "ctimer.h"
17+
#include "signalInflight.h"
1718
#include "dwarf.h"
1819
#include "flightRecorder.h"
1920
#include "itimer.h"
@@ -866,11 +867,6 @@ void Profiler::switchLibraryTrap(bool enable) {
866867
__atomic_store_n(_dlopen_entry, impl, __ATOMIC_RELEASE);
867868
}
868869

869-
void Profiler::enableEngines() {
870-
_cpu_engine->enableEvents(true);
871-
_wall_engine->enableEvents(true);
872-
}
873-
874870
void Profiler::disableEngines() {
875871
_cpu_engine->enableEvents(false);
876872
_wall_engine->enableEvents(false);
@@ -1402,8 +1398,6 @@ Error Profiler::start(Arguments &args, bool reset) {
14021398
_libs->updateBuildIds();
14031399
}
14041400

1405-
enableEngines();
1406-
14071401
// Refresher must be running before the trap fires: dlopen_hook's
14081402
// signal-context branch only marks dirty and relies on the refresher
14091403
// to call refresh() within REFRESH_INTERVAL_NS (500 ms).
@@ -1417,7 +1411,6 @@ Error Profiler::start(Arguments &args, bool reset) {
14171411
_num_context_attributes = args._context_attributes.size();
14181412
error = _jfr.start(args, reset);
14191413
if (error) {
1420-
disableEngines();
14211414
switchLibraryTrap(false);
14221415
_libs->stopRefresher();
14231416
return error;
@@ -1434,9 +1427,16 @@ Error Profiler::start(Arguments &args, bool reset) {
14341427
}
14351428
}
14361429
if ((_event_mask & EM_WALL) && _wall_engine != &noop_engine) {
1430+
// Enable wall clock BEFORE starting its pthread: timerLoopCommon() checks
1431+
// _enabled once at startup and exits early if false, so the pthread would
1432+
// otherwise die immediately and wall profiling would be silently dead for
1433+
// the recording. JFR is already initialized by this point (_jfr.start()
1434+
// ran above).
1435+
_wall_engine->enableEvents(true);
14371436
error = _wall_engine->start(args);
14381437
if (error) {
14391438
Log::warn("%s", error.message());
1439+
_wall_engine->enableEvents(false);
14401440
error = Error::OK; // recoverable
14411441
} else {
14421442
activated |= EM_WALL;
@@ -1492,6 +1492,15 @@ Error Profiler::start(Arguments &args, bool reset) {
14921492
// TODO: find a better way to resolve the thread name.
14931493
onThreadStart(nullptr, nullptr, nullptr);
14941494

1495+
// Enable CPU profiling last. CPU SIGPROF handlers write JFR events, so
1496+
// _enabled must not become true until _jfr.start() has completed; we also
1497+
// wait until after _cpu_engine->start() so the very first signal a thread
1498+
// ever sees finds the engine fully wired. Unlike wall clock, CPU handlers
1499+
// re-check _enabled on every signal, so enabling here (rather than before
1500+
// start()) does not lose samples.
1501+
// Paired with drainInflight() on the stop side.
1502+
_cpu_engine->enableEvents(true);
1503+
14951504
_state.store(RUNNING, std::memory_order_release);
14961505
_start_time = time(NULL);
14971506
__atomic_add_fetch(&_epoch, 1, __ATOMIC_RELAXED);
@@ -1516,8 +1525,22 @@ Error Profiler::stop() {
15161525
return Error("Profiler is not active");
15171526
}
15181527

1528+
// Order matters: disable engines first so the _enabled check inside signal
1529+
// handlers will fail for any new signal delivered from now on. drain() then
1530+
// waits for handlers that already passed the check to leave their JFR write
1531+
// path. Only once those are gone is it safe to run the rest of the teardown
1532+
// (engine stops, JFR teardown) without risking use-after-free.
15191533
disableEngines();
15201534

1535+
if (!SignalInflight::drain()) {
1536+
// Signal handlers stuck past the timeout. Leave state == RUNNING so the
1537+
// caller cannot start() against half-torn-down JFR and so engine stops
1538+
// (notably BaseWallClock::stop()'s pthread_join) are not double-invoked.
1539+
// The operation is idempotent on retry: disableEngines() above is an atomic
1540+
// store, and no other engine stop has run yet.
1541+
return Error("signal handlers did not drain; teardown skipped, retry stop()");
1542+
}
1543+
15211544
if (_event_mask & EM_ALLOC)
15221545
_alloc_engine->stop();
15231546
if (_event_mask & EM_NATIVEMEM)

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ class alignas(alignof(SpinLock)) Profiler {
127127
void switchLibraryTrap(bool enable);
128128
static void prewarmUnwinder();
129129

130-
void enableEngines();
131130
void disableEngines();
132131

133132
void onThreadStart(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 "signalInflight.h"
18+
#include "log.h"
19+
20+
#include <errno.h>
21+
#include <sched.h>
22+
#include <time.h>
23+
24+
alignas(64) int SignalInflight::_counter = 0;
25+
26+
// 200 ms: long enough for any legitimate signal handler to finish,
27+
// short enough to avoid a perceptible hang if a handler is somehow stuck.
28+
static const long DRAIN_TIMEOUT_NS = 200000000L;
29+
30+
bool SignalInflight::drain() {
31+
if (__atomic_load_n(&_counter, __ATOMIC_ACQUIRE) == 0) {
32+
return true; // fast path: nothing in flight
33+
}
34+
35+
struct timespec deadline;
36+
if (clock_gettime(CLOCK_MONOTONIC, &deadline) != 0) {
37+
Log::error("SignalInflight::drain: clock_gettime(CLOCK_MONOTONIC) failed (errno=%d). "
38+
"Skipping JFR teardown to prevent use-after-free.", errno);
39+
return false;
40+
}
41+
deadline.tv_nsec += DRAIN_TIMEOUT_NS;
42+
if (deadline.tv_nsec >= 1000000000L) {
43+
deadline.tv_sec += 1;
44+
deadline.tv_nsec -= 1000000000L;
45+
}
46+
47+
while (__atomic_load_n(&_counter, __ATOMIC_ACQUIRE) > 0) {
48+
struct timespec now;
49+
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
50+
Log::error("SignalInflight::drain: clock_gettime(CLOCK_MONOTONIC) failed (errno=%d). "
51+
"Skipping JFR teardown to prevent use-after-free.", errno);
52+
return false;
53+
}
54+
if (now.tv_sec > deadline.tv_sec ||
55+
(now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) {
56+
int remaining = __atomic_load_n(&_counter, __ATOMIC_ACQUIRE);
57+
Log::error("SignalInflight::drain: timed out after %ldms waiting for "
58+
"%d in-flight signal handler(s). Skipping JFR teardown to "
59+
"prevent use-after-free. This indicates a stuck signal handler.",
60+
DRAIN_TIMEOUT_NS / 1000000L, remaining);
61+
return false;
62+
}
63+
sched_yield();
64+
}
65+
return true;
66+
}

0 commit comments

Comments
 (0)