fix(profiler): address code-review findings on PR #540 · DataDog/java-profiler@6ef6687 · GitHub
Skip to content

Commit 6ef6687

Browse files
jbachorikclaude
andcommitted
fix(profiler): address code-review findings on PR #540
Code review surfaced 7 findings — 4 CONFIRMED, 3 PLAUSIBLE. This commit addresses all 7: #8 (CONFIRMED) — split isInSignalContext into a strict variant. dlopen_hook was treating null ProfiledThread as "in signal" and deferring refresh on every dlopen from uninstrumented JVM threads (VM Thread, JIT, GC), delaying wasmtime sigaction patching by up to 500 ms. Add isInTrackedSignalContext() that returns false on null (only true when one of our SignalHandlerScopes is positively on the stack); dlopen_hook now uses it so JVM-internal threads get synchronous refresh again. isInSignalContext() retains its conservative semantics for any future caller that wants AS-safe-by-default. #7 (CONFIRMED) — switchLibraryTrap was called before startRefresher despite the invariant comment. Reorder so the refresher is running before the trap can fire. #9 (CONFIRMED) — DEBUG_ASSERT_NOT_IN_SIGNAL was on the 1-arg and 2-arg Dictionary::lookup overloads but missed the 4-arg form that actually mallocs. bounded_lookup's runtime-decided for_insert path was uncovered. Move the assertion into the 4-arg lookup, gated on for_insert (read-only lookups are AS-safe). #4 (CONFIRMED) — Comments referenced "REFRESH_INTERVAL_NS (5s)" but the actual constant is 500 ms. Fix both stale mentions. #13 (PLAUSIBLE) — SIGNAL_HANDLER_GUARD_RELEASE before chaining leaves depth == 0 inside a chained handler that returns normally; DEBUG_ASSERT_NOT_IN_SIGNAL inside such a handler would not fire. Document the trade-off in segvHandler — the longjmp safety property is more important than the sanitizer coverage gap, which is bounded to third-party signal handler code we don't own. #2 (PLAUSIBLE) — refresherLoop used OS::sleep without an EINTR loop; any unmasked signal (SIGCHLD, SIGURG, RT signals) would cause premature ticks. Wrap the sleep in an explicit elapsed-time loop using OS::nanotime so the refresher ticks at 500 ms regardless of stray signals. #14 (PLAUSIBLE) — refresherLoop published _refresher_tid before blocking SIGPROF/SIGVTALRM; a stale per-thread timer from a previous lifecycle could fire on the refresher during the window. Block signals first, then publish the TID. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f77ba1a commit 6ef6687

6 files changed

Lines changed: 99 additions & 29 deletions

File tree

‎ddprof-lib/src/main/cpp/dictionary.cpp‎

Lines changed: 9 additions & 2 deletions

‎ddprof-lib/src/main/cpp/guards.cpp‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ bool isInSignalContext() {
3535
return pt == nullptr || pt->signalDepth() != 0;
3636
}
3737

38+
bool isInTrackedSignalContext() {
39+
ProfiledThread *pt = ProfiledThread::currentSignalSafe();
40+
// null ProfiledThread = no thread context; the SignalHandlerScope
41+
// never ran, so we have no positive evidence of a signal frame.
42+
// See header comment for the rationale of returning false here.
43+
return pt != nullptr && pt->signalDepth() != 0;
44+
}
45+
3846
SignalHandlerScope::SignalHandlerScope() : _active(true) {
3947
ProfiledThread *pt = ProfiledThread::currentSignalSafe();
4048
if (pt != nullptr) {

‎ddprof-lib/src/main/cpp/guards.h‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ int getInSignalDepth();
6161
// assume signal" — fail-safe for AS-safety gating).
6262
bool isInSignalContext();
6363

64+
// Returns true ONLY when we have positively tracked entering one of our
65+
// installed signal handlers on this thread. null ProfiledThread → false
66+
// (we have no way to know, so callers that prefer to do work
67+
// synchronously on uninstrumented threads — Profiler::dlopen_hook —
68+
// can use this.) Accepts the small theoretical risk that an
69+
// uninstrumented JVM-internal thread (VM Thread, JIT, GC) doing a dlopen
70+
// from inside a *foreign* signal handler will look like regular code;
71+
// prewarmUnwinder() covers the known libgcc_s case and JVM-internal
72+
// signal handlers are generally AS-safe by design.
73+
bool isInTrackedSignalContext();
74+
6475
// Internal RAII type — do not instantiate directly; use the macros below.
6576
class SignalHandlerScope {
6677
public:

‎ddprof-lib/src/main/cpp/libraries.cpp‎

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,13 @@ void Libraries::refresh() {
6969
void *Libraries::refresherLoop(void *arg) {
7070
Libraries *self = static_cast<Libraries *>(arg);
7171

72-
// Publish our TID so sampler thread-list enumerations can skip us.
73-
self->_refresher_tid.store(OS::threadId(), std::memory_order_release);
74-
75-
// Block profiling signals so this thread is never sampled. Without
76-
// this, SIGPROF can fire on the refresher and run the full stack-walk
77-
// path here — pure overhead, and in debug builds with
78-
// DDPROF_FORCE_STACKWALK_CRASH it inflates the SIGSEGV recovery count
79-
// enough to starve the test work thread and break
72+
// Block profiling signals BEFORE publishing our TID. Otherwise a
73+
// SIGPROF / SIGVTALRM armed for this thread (e.g. a stale per-thread
74+
// timer from a previous lifecycle, or an in-flight signal queued during
75+
// pthread_create) could fire on us between TID-publish and sigmask, and
76+
// run the full stack-walk path here — pure overhead, and in debug
77+
// builds with DDPROF_FORCE_STACKWALK_CRASH it inflates the SIGSEGV
78+
// recovery count enough to starve the test work thread and break
8079
// vmStackwalkerCrashRecoveryTest. SIGIO (WAKEUP_SIGNAL) is left
8180
// unmasked because stopRefresher() relies on it to interrupt sleep.
8281
sigset_t mask;
@@ -85,10 +84,27 @@ void *Libraries::refresherLoop(void *arg) {
8584
sigaddset(&mask, SIGVTALRM);
8685
pthread_sigmask(SIG_BLOCK, &mask, nullptr);
8786

87+
// Publish our TID so sampler thread-list enumerations can skip us.
88+
self->_refresher_tid.store(OS::threadId(), std::memory_order_release);
89+
8890
while (self->_refresher_running.load(std::memory_order_acquire)) {
89-
// OS::sleep uses nanosleep, which returns early on EINTR — the wakeup
90-
// signal sent from stopRefresher() will interrupt this immediately.
91-
OS::sleep(REFRESH_INTERVAL_NS);
91+
// Sleep until the next tick or until interrupted. OS::sleep wraps a
92+
// single nanosleep that returns early on EINTR; loop on the remaining
93+
// time so unrelated unmasked signals (SIGCHLD, debugger SIGSTOP/CONT,
94+
// etc.) do not cause premature ticks. The stopRefresher path sends
95+
// SIGIO and flips _refresher_running first, so the outer-loop check
96+
// catches the wake-up and exits without sleeping again.
97+
u64 remaining = REFRESH_INTERVAL_NS;
98+
while (remaining > 0 &&
99+
self->_refresher_running.load(std::memory_order_acquire)) {
100+
u64 before = OS::nanotime();
101+
OS::sleep(remaining);
102+
u64 elapsed = OS::nanotime() - before;
103+
if (elapsed >= remaining) {
104+
break;
105+
}
106+
remaining -= elapsed;
107+
}
92108
if (!self->_refresher_running.load(std::memory_order_acquire)) {
93109
break;
94110
}

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

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -777,20 +777,24 @@ void *Profiler::dlopen_hook(const char *filename, int flags) {
777777
void *result = dlopen(filename, flags);
778778

779779
if (result != NULL) {
780-
if (!isInSignalContext()) {
781-
// Normal (non-signal) dlopen: refresh synchronously so symbols in
782-
// the new library are resolved immediately.
780+
if (!isInTrackedSignalContext()) {
781+
// Either confirmed non-signal context, or no ProfiledThread on this
782+
// thread (uninstrumented JVM internals — VM Thread, JIT, GC). We
783+
// prefer synchronous refresh for the null-PT case too because (a)
784+
// those threads call dlopen synchronously during normal JVM
785+
// operation, and (b) wasmtime's broken sigaction patching depends
786+
// on switchLibraryTrap running its work inline (the original reason
787+
// for the trap). The residual risk — an uninstrumented thread
788+
// calling dlopen from inside a foreign signal handler — is small:
789+
// prewarmUnwinder() closes the known libgcc_s lazy-load case, and
790+
// mainstream JVM signal handlers are AS-safe by design.
783791
Libraries::instance()->refresh();
784792
} else {
785-
// Async-signal context. Profiler::prewarmUnwinder closes the known
786-
// libgcc_s case, but JVM/JIT lazy loads have been observed from
787-
// signal handlers under Graal on aarch64. refresh() must NOT run
788-
// here — parseLibraries acquires a Mutex and calls malloc, both
789-
// AS-unsafe. Mark the library set dirty; the Libraries refresher
790-
// thread picks it up within REFRESH_INTERVAL_NS (5s), bounding the
791-
// window during which the new library is unresolvable. flushJfr /
792-
// dump / stop also call refresh() unconditionally, so this works
793-
// even before the refresher thread starts.
793+
// Confirmed signal context (one of our SignalHandlerScopes is on
794+
// the stack). refresh() must NOT run here — parseLibraries
795+
// acquires a Mutex and calls malloc, both AS-unsafe. Mark the
796+
// library set dirty; the Libraries refresher thread picks it up
797+
// within REFRESH_INTERVAL_NS (500 ms).
794798
Libraries::instance()->markDirty();
795799
}
796800
}
@@ -837,6 +841,13 @@ void Profiler::segvHandler(int signo, siginfo_t *siginfo, void *ucontext) {
837841
// destructor, permanently leaking depth on the thread. Release the guard
838842
// before chaining so depth is correct whether the chained handler returns
839843
// or longjmps.
844+
//
845+
// Sanitizer-coverage note: this also means depth == 0 inside the chained
846+
// handler, so DEBUG_ASSERT_NOT_IN_SIGNAL() will NOT fire for AS-unsafe
847+
// code reachable from a chained handler that returns normally. This is
848+
// the lesser of two evils — leaking depth on longjmp would silently
849+
// break the production deferred-refresh gate, while the sanitizer gap
850+
// is bounded to third-party signal handler code we don't own.
840851
SIGNAL_HANDLER_GUARD();
841852
if (crashHandlerInternal(signo, siginfo, ucontext)) {
842853
return; // Handled — destructor decrements depth
@@ -1313,14 +1324,14 @@ Error Profiler::start(Arguments &args, bool reset) {
13131324

13141325
enableEngines();
13151326

1316-
// Always enable library trap to catch wasmtime loading and patch its broken sigaction
1317-
switchLibraryTrap(true);
1318-
13191327
// Refresher must be running before the trap fires: dlopen_hook's
13201328
// signal-context branch only marks dirty and relies on the refresher
1321-
// to call refresh() within ~5s.
1329+
// to call refresh() within REFRESH_INTERVAL_NS (500 ms).
13221330
_libs->startRefresher();
13231331

1332+
// Always enable library trap to catch wasmtime loading and patch its broken sigaction
1333+
switchLibraryTrap(true);
1334+
13241335
JfrMetadata::reset();
13251336
JfrMetadata::initialize(args._context_attributes);
13261337
_num_context_attributes = args._context_attributes.size();

‎ddprof-lib/src/test/cpp/signalSafety_ut.cpp‎

Lines changed: 17 additions & 0 deletions

0 commit comments

Comments
 (0)