Skip to content

Commit c5adab3

Browse files
authored
fix(profiler): make _class_map access signal-safe in walkVM (#512)
1 parent 82a700f commit c5adab3

7 files changed

Lines changed: 538 additions & 9 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ class Dictionary {
7575
void clear();
7676

7777
bool check(const char* key);
78+
// NOT signal-safe: the inserting lookup overloads call malloc/calloc on miss
79+
// (see allocateKey and the calloc in dictionary.cpp). Signal handlers must use
80+
// bounded_lookup(key, length, 0) instead, which never inserts and returns
81+
// INT_MAX on miss.
7882
unsigned int lookup(const char *key);
7983
unsigned int lookup(const char *key, size_t length);
8084
unsigned int bounded_lookup(const char *key, size_t length, int size_limit);

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,9 +1152,10 @@ void Recording::writeCpool(Buffer *buf) {
11521152
// constant pool count - bump each time a new pool is added
11531153
buf->put8(12);
11541154

1155-
// Profiler::instance()->classMap() provides access to non-locked _class_map
1156-
// instance The non-locked access is ok here as this code will never run
1157-
// concurrently to _class_map.clear()
1155+
// classMap() is shared across the dump (this thread) and the JVMTI shared-lock
1156+
// writers (Profiler::lookupClass and friends). writeClasses() holds
1157+
// classMapSharedGuard() for its full duration; the exclusive classMap()->clear()
1158+
// in Profiler::dump runs only after this method returns.
11581159
Lookup lookup(this, &_method_map, Profiler::instance()->classMap());
11591160
writeFrameTypes(buf);
11601161
writeThreadStates(buf);
@@ -1368,8 +1369,11 @@ void Recording::writeMethods(Buffer *buf, Lookup *lookup) {
13681369

13691370
void Recording::writeClasses(Buffer *buf, Lookup *lookup) {
13701371
std::map<u32, const char *> classes;
1371-
// no need to lock _classes as this code will never run concurrently with
1372-
// resetting that dictionary
1372+
// Hold classMapSharedGuard() for the full function. The const char* pointers
1373+
// stored in classes point into dictionary row storage; clear() frees that
1374+
// storage under the exclusive lock, so we must not release the shared lock
1375+
// until we have finished iterating.
1376+
auto guard = Profiler::instance()->classMapSharedGuard();
13731377
lookup->_classes->collect(classes);
13741378

13751379
buf->putVar64(T_CLASS);

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
#include <climits>
78
#include <cstdlib>
89
#include <setjmp.h>
910
#include "asyncSampleMutex.h"
@@ -530,8 +531,23 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex
530531
uintptr_t receiver = frame.jarg0();
531532
if (receiver != 0) {
532533
VMSymbol* symbol = VMKlass::fromOop(receiver)->name();
533-
u32 class_id = profiler->classMap()->lookup(symbol->body(), symbol->length());
534-
fillFrame(frames[depth++], BCI_ALLOC, class_id);
534+
// walkVM runs in a signal handler. _class_map is mutated
535+
// under _class_map_lock (shared by Profiler::lookupClass
536+
// inserters, exclusive by _class_map.clear() in the dump
537+
// path between unlockAll() and lock()). bounded_lookup
538+
// with size_limit=0 never inserts (no malloc), but it
539+
// still traverses row->next and reads row->keys, which
540+
// clear() concurrently frees. Take the lock shared via
541+
// try-lock; if an exclusive clear() is in progress, drop
542+
// the synthetic frame rather than read freed memory.
543+
auto guard = profiler->classMapTrySharedGuard();
544+
if (guard.ownsLock()) {
545+
u32 class_id = profiler->classMap()->bounded_lookup(
546+
symbol->body(), symbol->length(), 0);
547+
if (class_id != INT_MAX) {
548+
fillFrame(frames[depth++], BCI_ALLOC, class_id);
549+
}
550+
}
535551
}
536552
}
537553

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ class alignas(alignof(SpinLock)) Profiler {
204204
Engine *wallEngine() { return _wall_engine; }
205205

206206
Dictionary *classMap() { return &_class_map; }
207+
SharedLockGuard classMapSharedGuard() { return SharedLockGuard(&_class_map_lock); }
208+
BoundedOptionalSharedLockGuard classMapTrySharedGuard() { return BoundedOptionalSharedLockGuard(&_class_map_lock); }
207209
Dictionary *stringLabelMap() { return &_string_label_map; }
208210
Dictionary *contextValueMap() { return &_context_value_map; }
209211
u32 numContextAttributes() { return _num_context_attributes; }

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

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,30 @@ class alignas(DEFAULT_CACHE_LINE_SIZE) SpinLock {
4747
void unlock() { __sync_fetch_and_sub(&_lock, 1); }
4848

4949
bool tryLockShared() {
50+
// Spins while no exclusive lock is held and the CAS to acquire a shared
51+
// lock fails (due to concurrent reader contention). Returns false ONLY when
52+
// an exclusive lock is observed (_lock > 0); never returns false spuriously.
5053
int value;
5154
while ((value = __atomic_load_n(&_lock, __ATOMIC_ACQUIRE)) <= 0) {
5255
if (__sync_bool_compare_and_swap(&_lock, value, value - 1)) {
5356
return true;
5457
}
58+
spinPause();
59+
}
60+
return false;
61+
}
62+
63+
// Signal-safe variant: returns false after at most 5 CAS attempts.
64+
// Use only in signal-handler paths where spinning indefinitely is unsafe.
65+
bool tryLockSharedBounded() {
66+
for (int attempts = 0; attempts < 5; ++attempts) {
67+
int value = __atomic_load_n(&_lock, __ATOMIC_ACQUIRE);
68+
if (value > 0) {
69+
return false;
70+
}
71+
if (__sync_bool_compare_and_swap(&_lock, value, value - 1)) {
72+
return true;
73+
}
5574
}
5675
return false;
5776
}
@@ -88,9 +107,9 @@ class SharedLockGuard {
88107
class OptionalSharedLockGuard {
89108
SpinLock* _lock;
90109
public:
91-
OptionalSharedLockGuard(SpinLock* lock) : _lock(lock) {
110+
explicit OptionalSharedLockGuard(SpinLock* lock) : _lock(lock) {
92111
if (!_lock->tryLockShared()) {
93-
// Locking failed, no need to unlock.
112+
// Exclusive lock is held; no unlock needed. Only fails when an exclusive lock is observed.
94113
_lock = nullptr;
95114
}
96115
}
@@ -108,6 +127,33 @@ class OptionalSharedLockGuard {
108127
OptionalSharedLockGuard& operator=(OptionalSharedLockGuard&&) = delete;
109128
};
110129

130+
// Signal-safe variant of OptionalSharedLockGuard: uses bounded CAS retries
131+
// and may fail spuriously when racing other shared lockers. Use ONLY in
132+
// signal-handler paths where spinning indefinitely is unsafe; do NOT use
133+
// in hot recording paths where silent acquisition failures would drop events.
134+
class BoundedOptionalSharedLockGuard {
135+
SpinLock* _lock;
136+
public:
137+
explicit BoundedOptionalSharedLockGuard(SpinLock* lock) : _lock(lock) {
138+
if (!_lock->tryLockSharedBounded()) {
139+
// Locking failed (bounded retries exhausted or exclusive lock held); no unlock needed.
140+
_lock = nullptr;
141+
}
142+
}
143+
~BoundedOptionalSharedLockGuard() {
144+
if (_lock != nullptr) {
145+
_lock->unlockShared();
146+
}
147+
}
148+
bool ownsLock() { return _lock != nullptr; }
149+
150+
// Non-copyable and non-movable
151+
BoundedOptionalSharedLockGuard(const BoundedOptionalSharedLockGuard&) = delete;
152+
BoundedOptionalSharedLockGuard& operator=(const BoundedOptionalSharedLockGuard&) = delete;
153+
BoundedOptionalSharedLockGuard(BoundedOptionalSharedLockGuard&&) = delete;
154+
BoundedOptionalSharedLockGuard& operator=(BoundedOptionalSharedLockGuard&&) = delete;
155+
};
156+
111157
class ExclusiveLockGuard {
112158
private:
113159
SpinLock* _lock;

0 commit comments

Comments
 (0)