Never patch the profiler's own import table - #721
Open
hisener wants to merge 1 commit into
Open
Conversation
hisener
force-pushed
the
halil.sener/fix-library-patcher-self-patch
branch
from
August 6, 2026 15:34
e29143e to
5c7c2d5
Compare
This comment has been minimized.
This comment has been minimized.
hisener
force-pushed
the
halil.sener/fix-library-patcher-self-patch
branch
from
August 6, 2026 17:24
5c7c2d5 to
64845c6
Compare
zhengyu123
requested changes
Aug 6, 2026
zhengyu123
left a comment
Contributor
There was a problem hiding this comment.
_profiler_name is no longer used, so please remove it. You can also remove LibraryPatcher::initialize(), as its sole purpose was to initialize _profiler_name.
LibraryPatcher recognised its own library by comparing realpath(lib) with the profiler's path, and skipped that comparison entirely when realpath() returned nullptr. dd-trace-java extracts libjavaProfiler.so to a temporary file and unlinks it once loaded, so realpath() on the still-mapped path fails and the self-check reported "not self" - letting a library re-scan patch our own GOT entry for pthread_create. pthread_create_hook() reaches the real pthread_create() through that same entry, so the hook then called itself until the thread stack was exhausted: SIGSEGV with no hs_err file, since crash reporting needs stack of its own. Whether it happened depended on a re-scan landing after the unlink, which made it look random. Recognise our own library by mapped address range instead, which cannot fail. Every native library cache carries its mapping bounds, so no name comparison is kept as a fallback. Apply it at all three patch sites - pthread_create, sigaction and the socket functions - the last of which also passed a possibly-null _profiler_name to strcmp(). _profiler_name is now unused for identification, so drop it. The "initialized yet?" guards it doubled as are still needed and now read an explicit flag: patching must not start before Profiler::start(), because pthread_create_hook() routes new threads through Profiler::registerThread(), which crashes on a profiler that is not running. Environment: Datadog workspace Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hisener
force-pushed
the
halil.sener/fix-library-patcher-self-patch
branch
from
August 6, 2026 18:47
64845c6 to
19572a3
Compare
hisener
marked this pull request as ready for review
August 6, 2026 19:55
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR stops
LibraryPatcherfrom patching the profiler's own import table, which could turnpthread_create_hook()into unbounded recursion and kill the JVM.patch_library_unlocked()recognised its own library by comparingrealpath(lib)with the profiler's path, and skipped that comparison entirely whenrealpath()returnednullptr(libraryPatcher_linux.cpp#L419-L426). dd-trace-java extractslibjavaProfiler.soto a temporary file and unlinks it once loaded, sorealpath()on the still-mapped path fails, the self-check reports "not self", and a library re-scan patches our own GOT entry forpthread_create.pthread_create_hook()reaches the realpthread_create()through that same entry, so the hook then calls itself until the thread stack is exhausted:SIGSEGV, and nohs_errfile, because crash reporting needs stack of its own. Whether it happens depends on a re-scan landing after the unlink, which is what made these crashes look random.Diagnosed from core dumps of JVM test crashes:
pthread_create_hook+0xa8frames (the return address of its ownbl pthread_create@plt)si_signo=11 si_code=128 (SI_KERNEL) si_addr=0x0,pcinmalloc, andsp~2MB below the thread's stack basepthread_createJUMP_SLOTholdsbase+<pthread_create_hook>in every core inspectedReproduced at ~8-10% per run on a JVM test target with the profiler active. A standalone reproducer (unlink the extracted
.so,dlopento force a re-scan, then create threads) crashes dd-java-agent 1.65.0 downloaded straight from Maven Central, so this is not fixed in the latest release.The fix recognises our own library by mapped address range, which cannot fail:
self_anchor()returns the address of a function in this translation unit — a function rather than a static variable, because aCodeCachespans a library's executable segments (Symbols::parseLibrariesbuilds the bounds from/proc/self/maps), which do not cover.data/.bss. Since every native library cache carries those bounds, there is no name comparison and no fallback: per review, falling back to something known to be faulty is not worth keeping, and the library deletion that triggers it is going away regardless.Applied at all three patch sites —
pthread_create,sigaction, and the socket functions — the last of which also passed a possibly-null_profiler_nametostrcmp().Tests — new
libraryPatcher_ut.cpp(7 tests). They are verified to fail without the fix: swapping the originalrealpath/strcmplogic back in makes exactlyRecognisesSelfWhenItsLibraryFileWasUnlinkedandLeavesItsOwnPthreadCreateSlotUntouchedfail.StillPatchesForeignPthreadCreateSlotpasses in both states as a control, so the guard is shown to discriminate rather than to have quietly disabled patching. The full:ddprof-lib:gtestDebugsuite passes (54 test binaries, no failures).Also validated end to end against the reproducer, building the
.sotwice from this tree and swapping each into the same 1.65.0 agent jar:SIGSEGV, self-patched GOT, 5863 recursive framesThe control matters as much as the fixed run: it shows a
main-built library in a repackaged jar still crashes, so the repackaging is not what made the crash go away.Worth considering as a follow-up: having
pthread_create_hook()call the real function through a cacheddlsym(RTLD_NEXT, ...)pointer instead of its own PLT — aspatch_socket_functions()already does forsend/recv/write/read— would make a self-patch harmless rather than merely prevented. Not included here to keep this change off the thread-creation hot path.🤖 Generated with Claude Code