Uh oh!
There was an error while loading. Please reload this page.
fix(profiler): make _class_map access signal-safe in walkVM - #512
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
walkVM ran the inserting Dictionary::lookup from a signal handler, calling malloc/calloc — async-signal-unsafe. Recording::writeClasses also walked _class_map without holding _class_map_lock, racing the exclusive clear() that follows in Profiler::dump. Switch the vtable-target site to bounded_lookup(..., 0) and skip the synthetic frame on miss; hold _class_map_lock shared while writeClasses recurses into Dictionary::collect; document the signal-safety hazard on the inserting overloads; add a concurrent test for the lock discipline. Fixes PROF-14549. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f3e2ccb to
7880e7cComparejbachorik
commented
May 7, 2026
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:7880e7ce7f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Codex flagged a remaining race after the initial fix: walkVM's read-only bounded_lookup still traverses row->next and reads row->keys[j] without holding _class_map_lock, while Profiler::dump's exclusive _class_map.clear() runs in the unlockAll -> lock window and free()s those same nodes/strings. Take _class_map_lock shared via OptionalSharedLockGuard (try-lock so the signal handler never blocks). When an exclusive clear() is in progress, drop the synthetic vtable-target frame instead of dereferencing freed memory. Also add the missing <cstring> include in dictionary_concurrent_ut.cpp; strlen() previously worked only via a transitive include through gtest_crash_handler.h. Addresses review comments on PR #512. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI Test ResultsRun:#28552205361 | Commit:
Summary: Total: 0 | Passed: 0 | Failed: 0 Updated: 2026-07-01 22:39:57 UTC |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
flightRecorder.cpp: refer to classMap()/classMapLock() rather than the underlying _class_map/_class_map_lock fields. dictionary_concurrent_ut.cpp: drop PROF-14549 ticket reference. Co-Authored-By: muse <muse@noreply>
8f59e1a to
bc05222CompareUh oh!
There was an error while loading. Please reload this page.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
… vs dump clear Adds SignalHandlerBoundedLookupVsDumpClear: concurrent signal-handler threads (OptionalSharedLockGuard + bounded_lookup(0)) vs a dump thread (exclusive lock + dict.clear()), reproducing the PROF-14550 crash signature without the fix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codex flagged a remaining race after the initial fix: walkVM's read-only bounded_lookup still traverses row->next and reads row->keys[j] without holding _class_map_lock, while Profiler::dump's exclusive _class_map.clear() runs in the unlockAll -> lock window and free()s those same nodes/strings. Take _class_map_lock shared via OptionalSharedLockGuard (try-lock so the signal handler never blocks). When an exclusive clear() is in progress, drop the synthetic vtable-target frame instead of dereferencing freed memory. Also add the missing <cstring> include in dictionary_concurrent_ut.cpp; strlen() previously worked only via a transitive include through gtest_crash_handler.h. Addresses review comments on PR #512. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… handler and dump path (#516)
Uh oh!
There was an error while loading. Please reload this page.
…4618) Restores vtable_target frame capture in CPU-only recordings. After PR #512 made walkVM signal-safe (bounded_lookup with size_limit=0, no malloc), _class_map was only populated by allocation/liveness paths via lookupClass. In a CPU-only recording these paths never fire, so signal-safe lookups always missed and vtable-target frames were silently dropped. This change pre-populates _class_map from safe JVM-thread contexts: - Profiler::preregisterLoadedClasses(jvmtiEnv*) calls GetLoadedClasses + GetClassSignature + ObjectSampler::normalizeClassSignature, inserting every reference class into _class_map. Invoked at Profiler::start() (after clearAll()) and Profiler::dump() (after clearStandby()). TripleBufferedDictionary::lookup is thread-safe via RefCountGuard so no external locking is needed. - VM::ClassPrepare is moved out-of-line to vmEntry.cpp (vmEntry.h is included by profiler.h, hence the prior inline definition could not reference Profiler::instance()). The handler still calls loadMethodIDs first, then conditionally inserts the new class when _features.vtable_target is set. The hot signal-handler path in hotspotSupport.cpp is unchanged — pre-registration runs only on JVM threads, never under a signal. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…4618) Restores vtable_target frame capture in CPU-only recordings. After PR #512 made walkVM signal-safe (bounded_lookup with size_limit=0, no malloc), _class_map was only populated by allocation/liveness paths via lookupClass. In a CPU-only recording these paths never fire, so signal-safe lookups always missed and vtable-target frames were silently dropped. This change pre-populates _class_map from safe JVM-thread contexts: - Profiler::preregisterLoadedClasses(jvmtiEnv*) calls GetLoadedClasses + GetClassSignature + ObjectSampler::normalizeClassSignature, inserting every reference class into _class_map. Invoked at Profiler::start() (after clearAll()) and Profiler::dump() (after clearStandby()). TripleBufferedDictionary::lookup is thread-safe via RefCountGuard so no external locking is needed. - VM::ClassPrepare is moved out-of-line to vmEntry.cpp (vmEntry.h is included by profiler.h, hence the prior inline definition could not reference Profiler::instance()). The handler still calls loadMethodIDs first, then conditionally inserts the new class when _features.vtable_target is set. The hot signal-handler path in hotspotSupport.cpp is unchanged — pre-registration runs only on JVM threads, never under a signal. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…4618) Restores vtable_target frame capture in CPU-only recordings. After PR #512 made walkVM signal-safe (bounded_lookup with size_limit=0, no malloc), _class_map was only populated by allocation/liveness paths via lookupClass. In a CPU-only recording these paths never fire, so signal-safe lookups always missed and vtable-target frames were silently dropped. This change pre-populates _class_map from safe JVM-thread contexts: - Profiler::preregisterLoadedClasses(jvmtiEnv*) calls GetLoadedClasses + GetClassSignature + ObjectSampler::normalizeClassSignature, inserting every reference class into _class_map. Invoked at Profiler::start() (after clearAll()) and Profiler::dump() (after clearStandby()). TripleBufferedDictionary::lookup is thread-safe via RefCountGuard so no external locking is needed. - VM::ClassPrepare is moved out-of-line to vmEntry.cpp (vmEntry.h is included by profiler.h, hence the prior inline definition could not reference Profiler::instance()). The handler still calls loadMethodIDs first, then conditionally inserts the new class when _features.vtable_target is set. The hot signal-handler path in hotspotSupport.cpp is unchanged — pre-registration runs only on JVM threads, never under a signal. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
What does this PR do?:
Fixes a SIGSEGV in
Dictionary::clearreached viaProfiler::dumpby closing two correctness holes aroundProfiler::_class_map. Also covers the crash signature from PROF-14550 (same root cause, dump-path variant).walkVMathotspotSupport.cpp:388previously called the inserting 2-argDictionary::lookupfrom a signal handler, which callsmalloc/calloc(dictionary.cpp:25,114). That is async-signal-unsafe and could tear the malloc arena. Switched to the existing read-onlybounded_lookup(..., 0)guarded byOptionalSharedLockGuard(tryLockShared); on miss or contention the synthetic vtable-target frame is skipped — the JVMTI / Java-API path will populate the entry from a non-signal context.Recording::writeClassespreviously called_classes->collect(map)with no lock while JVMTI shared-lock writers (Profiler::lookupClass,ObjectSampler,LivenessTracker) could mutate the dictionary, and the dump path's exclusive_class_map.clear()ran shortly after. Now wrapped inSharedLockGuard guard(Profiler::instance()->classMapLock()). Multiple shared-lock holders coexist with CAS-based inserters; the exclusiveclear()waits untilwriteClassesreturns.Profiler::classMapLock()accessor next to the existingclassMap()accessor.Dictionary::lookupoverloads.writeCpoolandwriteClassesthat previously claimed concurrent access was already safe.Motivation:
PROF-14549 reports a SIGSEGV in
Dictionary::collectreached viaRecording::writeClassesduringProfiler::dump. PROF-14550 reports the same SIGSEGV inDictionary::clearreached viaProfiler::dumpdirectly. Both trace to the same root cause:walkVMcalling insertinglookupfrom a signal handler — malloc-arena tearing under signal preemption.writeClassesreading theDictionarychain unsynchronised against the dump-pathclear().The
vtable_targetfeature is on by default, so the buggy site is reachable on every CPU/wall/native-alloc sample whose top frame is a vtable stub.Additional Notes:
BCI_ALLOCreuse athotspotSupport.cpp:388is semantic overloading (frame marker, not allocation event). It was downported from upstream async-profiler and is preserved as-is here — only the signal-safety of the lookup is changed.src/stackWalker.cpp:399-405. Filing the equivalent fix upstream is a follow-up.Profiler::lookupClass. Eliminating the SIGSEGV outweighs the cosmetic loss.How to test the change?:
ddprof-lib/src/test/cpp/dictionary_concurrent_ut.cppcover:bounded_lookup(..., 0)on a miss returnsINT_MAXand does not insert.bounded_lookup(..., 0)on a hit returns the same id that the insertinglookupproduced.OptionalSharedLockGuard+bounded_lookup(0)"signal-handler" threads vs one exclusive-lock clear thread — regression gate for the PROF-14550 dump-path crash signature.For Datadog employees:
@DataDog/security-design-and-guidance.🤖 Generated with Claude Code