diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestPlugin.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestPlugin.kt index d2196cf8db..2b11eda2d8 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestPlugin.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestPlugin.kt @@ -1,3 +1,5 @@ +// Copyright 2026, Datadog, Inc. +// SPDX-License-Identifier: Apache-2.0 package com.datadoghq.native.gtest @@ -200,6 +202,11 @@ class GtestPlugin : Plugin { val buildGtestConfigTask = project.tasks.register("buildGtest${config.capitalizedName()}") { group = "build" description = "Compile and link all Google Tests for the ${config.name} build (no run)" + if (extension.buildNativeLibs.get()) { + // CI executes these binaries directly, so the build-only task must also produce + // the native fixtures that the binaries load at runtime. + dependsOn("buildNativeLibs") + } } // Compile all library sources ONCE for this config. Each test diff --git a/ddprof-lib/build.gradle.kts b/ddprof-lib/build.gradle.kts index b39dbfc249..a86e1dd8a3 100644 --- a/ddprof-lib/build.gradle.kts +++ b/ddprof-lib/build.gradle.kts @@ -1,3 +1,6 @@ +// Copyright 2026, Datadog, Inc. +// SPDX-License-Identifier: Apache-2.0 + import com.datadoghq.native.model.Platform import com.datadoghq.native.util.PlatformUtils import org.gradle.api.publish.maven.tasks.AbstractPublishToMaven @@ -36,6 +39,8 @@ nativeBuild { gtest { testSourceDir.set(layout.projectDirectory.dir("src/test/cpp")) mainSourceDir.set(layout.projectDirectory.dir("src/main/cpp")) + nativeLibsSourceDir.set(layout.projectDirectory.dir("src/test/resources/native-libs")) + nativeLibsOutputDir.set(rootProject.layout.buildDirectory.dir("test/resources/native-libs")) // Include paths for compilation val javaHome = PlatformUtils.javaHome() diff --git a/ddprof-lib/src/main/cpp/codeCache.cpp b/ddprof-lib/src/main/cpp/codeCache.cpp index ab96ee112d..5c0334795b 100644 --- a/ddprof-lib/src/main/cpp/codeCache.cpp +++ b/ddprof-lib/src/main/cpp/codeCache.cpp @@ -10,6 +10,8 @@ #include "safeAccess.h" #include +#include +#include #include #include #include @@ -60,7 +62,9 @@ CodeCache::CodeCache(const char *name, short lib_index, _build_id_len = 0; _load_bias = 0; - memset(_imports, 0, sizeof(_imports)); + memset(_import_offsets, 0, sizeof(_import_offsets)); + _incomplete_imports = 0; + _imports_finalized = true; _imports_patchable = imports_patchable; _dwarf_table = NULL; @@ -99,7 +103,10 @@ void CodeCache::copyFrom(const CodeCache& other) { } _load_bias = other._load_bias; - memset(_imports, 0, sizeof(_imports)); + _imports.clear(); + memset(_import_offsets, 0, sizeof(_import_offsets)); + _incomplete_imports = 0; + _imports_finalized = true; _imports_patchable = other._imports_patchable; _dwarf_table_length = other._dwarf_table_length; @@ -355,29 +362,44 @@ void CodeCache::findSymbolsByPrefix(std::vector &prefixes, } void CodeCache::saveImport(ImportId id, void** entry) { - for (int ty = 0; ty < NUM_IMPORT_TYPES; ty++) { - if (_imports[id][ty] == nullptr) { - _imports[id][ty] = entry; - return; - } + if (entry == nullptr || id < 0 || id >= NUM_IMPORTS) { + return; + } + try { + _imports.push_back({id, entry}); + _imports_finalized = false; + } catch (const std::bad_alloc&) { + _incomplete_imports |= 1ULL << id; } } void CodeCache::addImport(void **entry, const char *name) { switch (name[0]) { case 'a': - if (strcmp(name, "aligned_alloc") == 0) { + if (strcmp(name, "accept") == 0) { + saveImport(im_accept, entry); + } else if (strcmp(name, "accept4") == 0) { + saveImport(im_accept4, entry); + } else if (strcmp(name, "aligned_alloc") == 0) { saveImport(im_aligned_alloc, entry); } break; case 'c': if (strcmp(name, "calloc") == 0) { saveImport(im_calloc, entry); + } else if (strcmp(name, "close") == 0) { + saveImport(im_close, entry); + } else if (strcmp(name, "connect") == 0) { + saveImport(im_connect, entry); } break; case 'd': if (strcmp(name, "dlopen") == 0) { saveImport(im_dlopen, entry); + } else if (strcmp(name, "dup2") == 0) { + saveImport(im_dup2, entry); + } else if (strcmp(name, "dup3") == 0) { + saveImport(im_dup3, entry); } break; case 'f': @@ -385,6 +407,13 @@ void CodeCache::addImport(void **entry, const char *name) { saveImport(im_free, entry); } break; + case 'e': + if (strcmp(name, "epoll_wait") == 0) { + saveImport(im_epoll_wait, entry); + } else if (strcmp(name, "epoll_pwait") == 0) { + saveImport(im_epoll_pwait, entry); + } + break; case 'm': if (strcmp(name, "malloc") == 0) { saveImport(im_malloc, entry); @@ -399,6 +428,10 @@ void CodeCache::addImport(void **entry, const char *name) { saveImport(im_pthread_setspecific, entry); } else if (strcmp(name, "poll") == 0) { saveImport(im_poll, entry); + } else if (strcmp(name, "ppoll") == 0) { + saveImport(im_ppoll, entry); + } else if (strcmp(name, "pselect") == 0) { + saveImport(im_pselect, entry); } else if (strcmp(name, "posix_memalign") == 0) { saveImport(im_posix_memalign, entry); } @@ -408,6 +441,10 @@ void CodeCache::addImport(void **entry, const char *name) { saveImport(im_realloc, entry); } else if (strcmp(name, "recv") == 0) { saveImport(im_recv, entry); + } else if (strcmp(name, "recvfrom") == 0) { + saveImport(im_recvfrom, entry); + } else if (strcmp(name, "recvmsg") == 0) { + saveImport(im_recvmsg, entry); } else if (strcmp(name, "read") == 0) { saveImport(im_read, entry); } @@ -417,6 +454,8 @@ void CodeCache::addImport(void **entry, const char *name) { saveImport(im_send, entry); } else if (strcmp(name, "sigaction") == 0) { saveImport(im_sigaction, entry); + } else if (strcmp(name, "select") == 0) { + saveImport(im_select, entry); } break; case 'w': @@ -427,46 +466,97 @@ void CodeCache::addImport(void **entry, const char *name) { } } -void **CodeCache::findImport(ImportId id) { - if (!_imports_patchable) { - makeImportsPatchable(); - _imports_patchable = true; +void CodeCache::finalizeImports() { + if (_imports_finalized) { + return; } - return _imports[id][PRIMARY]; -} -void CodeCache::patchImport(ImportId id, void *hook_func) { - if (!_imports_patchable) { - makeImportsPatchable(); - _imports_patchable = true; + std::sort(_imports.begin(), _imports.end(), [](const ImportLocation& a, + const ImportLocation& b) { + if (a._id != b._id) { + return a._id < b._id; } + return reinterpret_cast(a._location) < + reinterpret_cast(b._location); + }); + _imports.erase(std::unique(_imports.begin(), _imports.end(), + [](const ImportLocation& a, const ImportLocation& b) { + return a._id == b._id && a._location == b._location; + }), _imports.end()); + + memset(_import_offsets, 0, sizeof(_import_offsets)); + for (const ImportLocation& entry : _imports) { + _import_offsets[entry._id + 1]++; + } + for (int id = 0; id < NUM_IMPORTS; id++) { + _import_offsets[id + 1] += _import_offsets[id]; + } + _imports_finalized = true; +} - for (int ty = 0; ty < NUM_IMPORT_TYPES; ty++) {void **entry = _imports[id][ty]; - if (entry != NULL) { - *entry = hook_func; - }} +size_t CodeCache::importCount(ImportId id) { + if (id < 0 || id >= NUM_IMPORTS) { + return 0; + } + finalizeImports(); + return _import_offsets[id + 1] - _import_offsets[id]; } -void CodeCache::makeImportsPatchable() { - void **min_import = (void **)-1; - void **max_import = NULL; - for (int i = 0; i < NUM_IMPORTS; i++) { - for (int j = 0; j < NUM_IMPORT_TYPES; j++) { - void** entry = _imports[i][j]; - if (entry == NULL) continue; - if (entry < min_import) +bool CodeCache::importsComplete(ImportId id) const { + return id >= 0 && id < NUM_IMPORTS && + (_incomplete_imports & (1ULL << id)) == 0; +} + +bool CodeCache::prepareImportsForPatch() { + if (_imports_patchable) { + return true; + } + return makeImportsPatchable(); +} + +void **CodeCache::findImport(ImportId id, size_t index) { + if (id < 0 || id >= NUM_IMPORTS || index >= importCount(id)) { + return nullptr; + } + if (!prepareImportsForPatch()) { + return nullptr; + } + return _imports[_import_offsets[id] + index]._location; +} + +bool CodeCache::patchImport(ImportId id, void *hook_func) { + if (!prepareImportsForPatch()) { + return false; + } + size_t count = importCount(id); + for (size_t index = 0; index < count; index++) { + *_imports[_import_offsets[id] + index]._location = hook_func; + } + return true; +} + +bool CodeCache::makeImportsPatchable() { + finalizeImports(); + uintptr_t min_import = UINTPTR_MAX; + uintptr_t max_import = 0; + for (const ImportLocation& import : _imports) { + uintptr_t entry = reinterpret_cast(import._location); + if (entry < min_import) min_import = entry; if (entry > max_import) max_import = entry; - } } - if (max_import != NULL) { - uintptr_t patch_start = (uintptr_t)min_import & ~OS::page_mask; - uintptr_t patch_end = (uintptr_t)max_import & ~OS::page_mask; - mprotect((void *)patch_start, patch_end - patch_start + OS::page_size, - PROT_READ | PROT_WRITE); + if (max_import != 0) { + uintptr_t patch_start = min_import & ~OS::page_mask; + uintptr_t patch_end = max_import & ~OS::page_mask; + if (mprotect((void *)patch_start, patch_end - patch_start + OS::page_size, + PROT_READ | PROT_WRITE) != 0) { + return false; + } } + _imports_patchable = true; + return true; } void CodeCache::setDwarfTable(FrameDesc *table, int length, const FrameDesc &default_frame) { @@ -527,4 +617,3 @@ void CodeCache::setBuildId(const char* build_id, size_t build_id_len) { } } } - diff --git a/ddprof-lib/src/main/cpp/codeCache.h b/ddprof-lib/src/main/cpp/codeCache.h index d8ac7d661e..7d8964e5e9 100644 --- a/ddprof-lib/src/main/cpp/codeCache.h +++ b/ddprof-lib/src/main/cpp/codeCache.h @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -28,6 +29,8 @@ const int MAX_NATIVE_LIBS = 2048; enum ImportId { im_dlopen, + im_dup2, + im_dup3, im_pthread_create, im_pthread_exit, im_pthread_setspecific, @@ -43,15 +46,20 @@ enum ImportId { im_recv, im_write, im_read, + im_close, + im_connect, + im_accept, + im_accept4, + im_recvfrom, + im_recvmsg, + im_epoll_wait, + im_epoll_pwait, + im_ppoll, + im_select, + im_pselect, NUM_IMPORTS }; -enum ImportType { - PRIMARY, - SECONDARY, - NUM_IMPORT_TYPES -}; - enum Mark { MARK_VM_RUNTIME = 1, MARK_INTERPRETER = 2, @@ -137,6 +145,13 @@ class CodeBlob { class CodeCache { private: + static_assert(NUM_IMPORTS <= 64, "import completeness mask must cover every import"); + + struct ImportLocation { + ImportId _id; + void** _location; + }; + char *_name; short _lib_index; const void *_min_address; @@ -152,7 +167,10 @@ class CodeCache { size_t _build_id_len; // Build-id length in bytes (raw, not hex string length) uintptr_t _load_bias; // Load bias (image_base - file_base address) - void **_imports[NUM_IMPORTS][NUM_IMPORT_TYPES]; + std::vector _imports; + size_t _import_offsets[NUM_IMPORTS + 1]; + u64 _incomplete_imports; + bool _imports_finalized; bool _imports_patchable; bool _debug_symbols; @@ -177,7 +195,8 @@ class CodeCache { std::atomic _published; void expand(); - void makeImportsPatchable(); + void finalizeImports(); + bool makeImportsPatchable(); void saveImport(ImportId id, void** entry); void copyFrom(const CodeCache& other); @@ -264,8 +283,11 @@ class CodeCache { } void addImport(void **entry, const char *name); - void **findImport(ImportId id); - void patchImport(ImportId, void *hook_func); + size_t importCount(ImportId id); + bool importsComplete(ImportId id) const; + bool prepareImportsForPatch(); + void **findImport(ImportId id, size_t index = 0); + bool patchImport(ImportId, void *hook_func); CodeBlob *findBlob(const char *name); CodeBlob *findBlobByAddress(const void *address); diff --git a/ddprof-lib/src/main/cpp/counters.h b/ddprof-lib/src/main/cpp/counters.h index e55fc3a50b..5c812a0422 100644 --- a/ddprof-lib/src/main/cpp/counters.h +++ b/ddprof-lib/src/main/cpp/counters.h @@ -135,6 +135,8 @@ X(JVMTI_STACKS_REQUESTED, "jvmti_stacks_requested") \ X(NATIVE_TRACE_HOOK_PREFIX_NOT_FOUND, "native_trace_hook_prefix_not_found") \ X(NATIVE_HOOK_MARK_RESOLVE_FAILED, "native_hook_mark_resolve_failed") \ + X(NATIVE_IO_STANDARD_HOOKS_PATCHED, "native_io_standard_hooks_patched") \ + X(NATIVE_IO_IBM_BRIDGE_HOOKS_PATCHED, "native_io_ibm_bridge_hooks_patched") \ X(JVMTI_STACKS_FAILED_WRONG_PHASE, "jvmti_stacks_failed_wrong_phase") \ X(JVMTI_STACKS_FAILED_OTHER, "jvmti_stacks_failed_other") \ /* Delegated stacks dropped at slot-lock. Rec-lock drops from all recording \ diff --git a/ddprof-lib/src/main/cpp/flightRecorder.cpp b/ddprof-lib/src/main/cpp/flightRecorder.cpp index da8cb46fee..4e2be87f77 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.cpp +++ b/ddprof-lib/src/main/cpp/flightRecorder.cpp @@ -1510,7 +1510,7 @@ void Recording::writeFrameTypes(Buffer *buf) { void Recording::writeThreadStates(Buffer *buf) { buf->putVar64(T_THREAD_STATE); - buf->put8(10); + buf->put8(11); buf->put8(static_cast(OSThreadState::UNKNOWN)); buf->putUtf8("UNKNOWN"); buf->put8(static_cast(OSThreadState::NEW)); @@ -1531,6 +1531,8 @@ void Recording::writeThreadStates(Buffer *buf) { buf->putUtf8("TERMINATED"); buf->put8(static_cast(OSThreadState::SYSCALL)); buf->putUtf8("SYSCALL"); + buf->put8(static_cast(OSThreadState::IO_WAIT)); + buf->putUtf8("IO_WAIT"); flushIfNeeded(buf); } diff --git a/ddprof-lib/src/main/cpp/libraryPatcher.h b/ddprof-lib/src/main/cpp/libraryPatcher.h index 70be3659b7..fd3371dfa1 100644 --- a/ddprof-lib/src/main/cpp/libraryPatcher.h +++ b/ddprof-lib/src/main/cpp/libraryPatcher.h @@ -1,21 +1,49 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + #ifndef _LIBRARYPATCHER_H #define _LIBRARYPATCHER_H #include "codeCache.h" #include "spinLock.h" + #include +#include #ifdef __linux__ -// Patch libraries' @plt entries +// Patch libraries' imported function relocation slots. typedef struct _patchEntry { CodeCache* _lib; - // library's @plt location + // Library import location. void** _location; // original function void* _func; } PatchEntry; +// Native I/O patching only needs the slot and its exact pre-patch value. +// Each retained import location is tracked independently for restoration. +typedef struct _socketPatchEntry { + void** _location; + void* _func; +} SocketPatchEntry; + +typedef struct _socketPatchedLibrary { + const void* _image_base; + void* _unload_protection; + size_t _first_patch; + size_t _patch_count; +} SocketPatchedLibrary; + +enum SocketPatchTarget : u8 { + SOCKET_PATCH_NONE = 0, + SOCKET_PATCH_STANDARD_JDK_NETWORK, + SOCKET_PATCH_IBM_JCL_BRIDGE +}; + +const int SOCKET_BASE_TABLE_SIZE = MAX_NATIVE_LIBS * 2; class LibraryPatcher { private: @@ -29,36 +57,52 @@ class LibraryPatcher { static PatchEntry _sigaction_entries[MAX_NATIVE_LIBS]; static int _sigaction_size; - // Separate tracking for socket (send/recv/write/read) patches. - // Each library can contribute up to 4 GOT slots (send/recv/write/read). - static PatchEntry _socket_entries[4 * MAX_NATIVE_LIBS]; - static int _socket_size; + // Separate tracking for native I/O patches. + // Each library can contribute any number of retained import slots per I/O hook. + static std::vector _socket_entries; + static std::vector _socket_libraries; + static const void* _socket_bases[SOCKET_BASE_TABLE_SIZE]; static void patch_library_unlocked(CodeCache* lib); static void patch_pthread_create(); static void patch_pthread_setspecific(); static void patch_sigaction_in_library(CodeCache* lib); + static bool socket_library_patched_unlocked(const void* image_base); + static void remember_socket_library_unlocked(const void* image_base); + static void unpatch_socket_functions_unlocked( + std::vector& libraries_to_release); + static void release_socket_libraries( + std::vector& libraries); public: - // True while socket hooks are installed; read by Profiler::dlopen_hook + // True while native I/O hooks are installed; read by library refresh paths // to decide whether to re-patch after a new library is loaded. // Set to true after the first batch of libraries is patched in patch_socket_functions(). - // Libraries loaded after profiler start are picked up on the next dlopen_hook call, + // Libraries loaded after profiler start are picked up on the next refresh, // which calls install_socket_hooks() to patch them if _socket_active is true. - // Low-probability race: stop() is called only on JVM exit; atomic is zero-cost insurance. + // start()/stop() and the library refresher can observe this state from different + // threads, so keep the flag atomic even though stop normally happens at JVM shutdown. static std::atomic _socket_active; static void initialize(); static void patch_libraries(); static void unpatch_libraries(); static void patch_sigaction(); - static bool patch_socket_functions(); + static bool patch_socket_functions(bool require_active = false); static void unpatch_socket_functions(); - // Called from Profiler::dlopen_hook after a new library is loaded. - // No-op when socket hooks are not active. - static inline void install_socket_hooks() { - if (_socket_active.load(std::memory_order_acquire)) { - patch_socket_functions(); - } - } + static bool unpatch_socket_functions_if_inactive(); +#ifdef UNIT_TEST + static int patch_socket_import_for_test(CodeCache* lib, ImportId import_id, + void* hook, const char* name, + bool retain_library = false); + static int socket_patch_count_for_test(); + static int socket_library_count_for_test(); + static SocketPatchTarget socket_patch_target_for_test( + CodeCache* lib, const char* library_name, bool in_jdk_directory); + static void* socket_hook_for_target_for_test(SocketPatchTarget target, + int hook_index); +#endif + // Called after a new library is loaded and the library list is refreshed. + // No-op when native I/O hooks are not active. + static void install_socket_hooks(); }; #else @@ -69,8 +113,14 @@ class LibraryPatcher { static void patch_libraries() { } static void unpatch_libraries() { } static void patch_sigaction() { } - static bool patch_socket_functions() { return false; } + static bool patch_socket_functions(bool require_active = false) { + (void)require_active; + return false; + } static void unpatch_socket_functions() { } + static bool unpatch_socket_functions_if_inactive() { + return false; + } static void install_socket_hooks() { } }; diff --git a/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp b/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp index 1c08286d22..fc781087fd 100644 --- a/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp +++ b/ddprof-lib/src/main/cpp/libraryPatcher_linux.cpp @@ -9,16 +9,22 @@ #include "counters.h" #include "guards.h" #include "jvmThread.h" +#include "nativeSocketInterposer.h" #include "nativeSocketSampler.h" #include "profiler.h" +#include "symbols.h" +#include #include #include +#include #include #include #include #include #include +#include +#include typedef void* (*func_start_routine)(void*); @@ -28,10 +34,130 @@ PatchEntry LibraryPatcher::_patched_entries[MAX_NATIVE_LIBS]; int LibraryPatcher::_size = 0; PatchEntry LibraryPatcher::_sigaction_entries[MAX_NATIVE_LIBS]; int LibraryPatcher::_sigaction_size = 0; -PatchEntry LibraryPatcher::_socket_entries[4 * MAX_NATIVE_LIBS]; -int LibraryPatcher::_socket_size = 0; +std::vector LibraryPatcher::_socket_entries; +std::vector LibraryPatcher::_socket_libraries; +const void* LibraryPatcher::_socket_bases[SOCKET_BASE_TABLE_SIZE] = {}; std::atomic LibraryPatcher::_socket_active{false}; +static_assert(SOCKET_BASE_TABLE_SIZE > 0 && + (SOCKET_BASE_TABLE_SIZE & (SOCKET_BASE_TABLE_SIZE - 1)) == 0, + "socket DSO lookup table size must be a power of two"); + +static const char* library_basename(const char* path) { + if (path == nullptr) { + return nullptr; + } + const char* name = strrchr(path, '/'); + return name == nullptr ? path : name + 1; +} + +static SocketPatchTarget socket_patch_target_for_library( + CodeCache* lib, const char* name, bool in_jdk_directory) { + if (lib == nullptr || name == nullptr || !in_jdk_directory) { + return SOCKET_PATCH_NONE; + } + + if (strcmp(name, "libnet.so") == 0 || strcmp(name, "libnio.so") == 0) { + return SOCKET_PATCH_STANDARD_JDK_NETWORK; + } + + // IBM Java 8 routes java.net through JCL_* exports in libjava before + // reaching libc. Require the complete observed bridge signature so an + // OpenJDK libjava or an arbitrary JNI DSO is never selected by name alone. + if (strcmp(name, "libjava.so") == 0 && + lib->findSymbol("JCL_Send") != nullptr && + lib->findSymbol("JCL_Recv") != nullptr && + lib->findSymbol("JCL_Connect") != nullptr && + lib->findSymbol("JCL_Accept") != nullptr) { + return SOCKET_PATCH_IBM_JCL_BRIDGE; + } + + return SOCKET_PATCH_NONE; +} + +static SocketPatchTarget socket_patch_target( + CodeCache* lib, const char* path, const char* jdk_library_directory) { + if (path == nullptr || jdk_library_directory == nullptr) { + return SOCKET_PATCH_NONE; + } + const char* path_separator = strrchr(path, '/'); + if (path_separator == nullptr) { + return SOCKET_PATCH_NONE; + } + size_t directory_length = static_cast(path_separator - path); + bool in_jdk_directory = + strlen(jdk_library_directory) == directory_length && + strncmp(path, jdk_library_directory, directory_length) == 0; + return socket_patch_target_for_library( + lib, library_basename(path), in_jdk_directory); +} + +static void* socket_hook_for_target( + SocketPatchTarget target, + const NativeSocketInterposer::NativeIoHookSpec& hook) { + return target == SOCKET_PATCH_IBM_JCL_BRIDGE ? hook.fork_safe_hook + : hook.hook; +} + +static bool resolve_jdk_library_directory(char* directory) { + CodeCache* java_library = Libraries::instance()->findLibraryByName("libjava.so"); + if (java_library != nullptr && java_library->name() != nullptr && + strcmp(library_basename(java_library->name()), "libjava.so") == 0 && + realpath(java_library->name(), directory) != nullptr) { + char* separator = strrchr(directory, '/'); + if (separator != nullptr) { + *separator = '\0'; + return true; + } + } + + // libjava may be loaded lazily. Supported JDK layouts place libjvm in a + // subdirectory immediately below the directory containing the other native + // JDK libraries. + CodeCache* jvm_library = Libraries::instance()->findLibraryByName("libjvm.so"); + if (jvm_library == nullptr || jvm_library->name() == nullptr || + strcmp(library_basename(jvm_library->name()), "libjvm.so") != 0 || + realpath(jvm_library->name(), directory) == nullptr) { + return false; + } + char* separator = strrchr(directory, '/'); + if (separator == nullptr) { + return false; + } + *separator = '\0'; + separator = strrchr(directory, '/'); + if (separator == nullptr) { + return false; + } + *separator = '\0'; + return true; +} + +bool LibraryPatcher::socket_library_patched_unlocked(const void* image_base) { + size_t slot = (reinterpret_cast(image_base) >> 12) & + (SOCKET_BASE_TABLE_SIZE - 1); + for (int probe = 0; probe < SOCKET_BASE_TABLE_SIZE; probe++) { + const void* value = _socket_bases[slot]; + if (value == nullptr) { + return false; + } + if (value == image_base) { + return true; + } + slot = (slot + 1) & (SOCKET_BASE_TABLE_SIZE - 1); + } + return false; +} + +void LibraryPatcher::remember_socket_library_unlocked(const void* image_base) { + size_t slot = (reinterpret_cast(image_base) >> 12) & + (SOCKET_BASE_TABLE_SIZE - 1); + while (_socket_bases[slot] != nullptr && _socket_bases[slot] != image_base) { + slot = (slot + 1) & (SOCKET_BASE_TABLE_SIZE - 1); + } + _socket_bases[slot] = image_base; +} + void LibraryPatcher::initialize() { if (_profiler_name == nullptr) { Dl_info info; @@ -538,7 +664,41 @@ void LibraryPatcher::patch_sigaction() { } } -bool LibraryPatcher::patch_socket_functions() { +class SocketPatchCandidate { +public: + CodeCache* _lib; + UnloadProtection _protection; + size_t _patch_count; + SocketPatchTarget _target; + + SocketPatchCandidate(CodeCache* lib, UnloadProtection&& protection, + size_t patch_count, SocketPatchTarget target) + : _lib(lib), _protection(std::move(protection)), + _patch_count(patch_count), _target(target) {} + + SocketPatchCandidate(const SocketPatchCandidate&) = delete; + SocketPatchCandidate& operator=(const SocketPatchCandidate&) = delete; + SocketPatchCandidate(SocketPatchCandidate&&) noexcept = default; + SocketPatchCandidate& operator=(SocketPatchCandidate&&) noexcept = default; +}; + +static bool mappingMatches(const CodeCache* lib) { + if (lib->imageBase() == nullptr) { + return false; + } + Dl_info info; + return dladdr(lib->imageBase(), &info) != 0 && + info.dli_fbase == lib->imageBase(); +} + +bool LibraryPatcher::patch_socket_functions(bool require_active) { + auto disable_and_unpatch = []() { + NativeSocketInterposer::instance()->disableAfterPatchFailure(); + NativeSocketSampler::disableAfterPatchFailure(); + LibraryPatcher::unpatch_socket_functions(); + return false; + }; + // Resolve the real libc symbols ONCE at first call and cache them. On a // restart cycle (stop()→start()) we MUST NOT re-resolve via RTLD_NEXT: if // any GOT slot in another DSO was missed during unpatch (e.g. its CodeCache @@ -552,167 +712,377 @@ bool LibraryPatcher::patch_socket_functions() { // May resolve to an LD_PRELOAD interposer (e.g. libasan) — intentional. // On musl, RTLD_NEXT returns NULL when libc is loaded before this DSO in the // link map; fall back to RTLD_DEFAULT which finds symbols globally. - // The four statics and the `cached` flag are written once and then - // read-only. They live outside the ExclusiveLockGuard intentionally (dlsym - // must not be called while holding _lock because dlsym may acquire the + // The cached originals and the `has_cached_original` flag are written once + // and then read-only. They live outside the ExclusiveLockGuard intentionally + // (dlsym must not be called while holding _lock because dlsym may acquire the // linker lock, which is also acquired during dlopen — inverting the order // would deadlock). Guard the one-time init with a dedicated once_flag so // that concurrent callers serialise on the dlsym block rather than racing // to write the statics. - static NativeSocketSampler::send_fn cached_send = nullptr; - static NativeSocketSampler::recv_fn cached_recv = nullptr; - static NativeSocketSampler::write_fn cached_write = nullptr; - static NativeSocketSampler::read_fn cached_read = nullptr; + static void* cached_originals[NativeSocketInterposer::NUM_NATIVE_IO_HOOKS] = {}; + static bool has_cached_original = false; static std::once_flag dlsym_once; + + const NativeSocketInterposer::NativeIoHookSpec* hooks = + NativeSocketInterposer::hookSpecs(); + std::call_once(dlsym_once, [&]() { - cached_send = (NativeSocketSampler::send_fn) dlsym(RTLD_NEXT, "send"); - if (!cached_send) cached_send = (NativeSocketSampler::send_fn) dlsym(RTLD_DEFAULT, "send"); - cached_recv = (NativeSocketSampler::recv_fn) dlsym(RTLD_NEXT, "recv"); - if (!cached_recv) cached_recv = (NativeSocketSampler::recv_fn) dlsym(RTLD_DEFAULT, "recv"); - cached_write = (NativeSocketSampler::write_fn) dlsym(RTLD_NEXT, "write"); - if (!cached_write) cached_write = (NativeSocketSampler::write_fn) dlsym(RTLD_DEFAULT, "write"); - cached_read = (NativeSocketSampler::read_fn) dlsym(RTLD_NEXT, "read"); - if (!cached_read) cached_read = (NativeSocketSampler::read_fn) dlsym(RTLD_DEFAULT, "read"); - // If dlsym resolves to one of our own hooks the linker is already serving - // the patched copy. Null the pointers so the early-return below fires. - if (cached_send == &NativeSocketSampler::send_hook || - cached_recv == &NativeSocketSampler::recv_hook || - cached_write == &NativeSocketSampler::write_hook || - cached_read == &NativeSocketSampler::read_hook) { - TEST_LOG("patch_socket_functions dlsym returned hook address; refusing to self-reference"); - cached_send = nullptr; cached_recv = nullptr; - cached_write = nullptr; cached_read = nullptr; + for (int hook_index = 0; hook_index < NativeSocketInterposer::NUM_NATIVE_IO_HOOKS; + hook_index++) { + void* original = dlsym(RTLD_NEXT, hooks[hook_index].name); + if (original == nullptr) { + original = dlsym(RTLD_DEFAULT, hooks[hook_index].name); + } + if (original == hooks[hook_index].hook || + original == hooks[hook_index].fork_safe_hook) { + TEST_LOG("patch_socket_functions dlsym returned hook address for %s", + hooks[hook_index].name); + // If dlsym resolves to one of our own hooks the linker is already serving + // the patched copy. Null this pointer so the hook is not installed. + original = nullptr; + } + cached_originals[hook_index] = original; + has_cached_original |= original != nullptr; + if (original != nullptr) { + NativeSocketInterposer::setOriginalFunction(hook_index, original); + } } + NativeSocketSampler::setOriginalFunctions( + reinterpret_cast( + cached_originals[NativeSocketInterposer::HOOK_SEND]), + reinterpret_cast( + cached_originals[NativeSocketInterposer::HOOK_RECV]), + reinterpret_cast( + cached_originals[NativeSocketInterposer::HOOK_WRITE]), + reinterpret_cast( + cached_originals[NativeSocketInterposer::HOOK_READ])); }); - auto pre_send = cached_send; - auto pre_recv = cached_recv; - auto pre_write = cached_write; - auto pre_read = cached_read; - TEST_LOG("patch_socket_functions dlsym send=%p recv=%p write=%p read=%p", - (void*)pre_send, (void*)pre_recv, (void*)pre_write, (void*)pre_read); - if (!pre_send || !pre_recv || !pre_write || !pre_read) { - TEST_LOG("patch_socket_functions EARLY RETURN: at least one dlsym returned NULL"); - return false; + + if (!has_cached_original) { + Log::warn("native I/O hooks disabled: all original symbol lookups failed"); + return disable_and_unpatch(); } + // Publish the process that owns profiler state before any fork-safe hook can + // become reachable. A post-fork child observes a different getpid() value + // and calls the original libc function without touching profiler state. + NativeSocketInterposer::setHookOwnerPid(getpid()); + const CodeCacheArray& native_libs = Libraries::instance()->native_libs(); int num_of_libs = native_libs.count(); - // Pre-resolve all library paths before acquiring the lock: realpath() may - // block on I/O and must not be called while holding _lock. - // We only need the is-self flag per library, so avoid a huge stack allocation. - static_assert(MAX_NATIVE_LIBS > 0, "MAX_NATIVE_LIBS must be positive"); - bool is_self[MAX_NATIVE_LIBS]; - int capped = (num_of_libs <= MAX_NATIVE_LIBS) ? num_of_libs : MAX_NATIVE_LIBS; + int capped = num_of_libs <= MAX_NATIVE_LIBS ? num_of_libs : MAX_NATIVE_LIBS; + char jdk_library_directory[PATH_MAX]; + if (!resolve_jdk_library_directory(jdk_library_directory)) { + Log::warn("native I/O hooks disabled: cannot locate the JDK native library directory"); + return disable_and_unpatch(); + } + std::vector candidates; + try { + candidates.reserve(capped); + } catch (const std::bad_alloc&) { + Log::warn("native I/O hooks disabled: unable to allocate DSO candidate table"); + return disable_and_unpatch(); + } + for (int index = 0; index < capped; index++) { CodeCache* lib = native_libs.at(index); - is_self[index] = false; - if (lib == nullptr || lib->name() == nullptr) continue; + if (lib == nullptr || lib->name() == nullptr) { + continue; + } char path[PATH_MAX]; - char* rp = realpath(lib->name(), path); - is_self[index] = (rp != nullptr && strcmp(rp, _profiler_name) == 0); - } - - ExclusiveLockGuard locker(&_lock); - // Re-check under the lock only on re-entry (when hooks are already installed): - // a concurrent unpatch_socket_functions() may have cleared _socket_active - // between the acquire-load in install_socket_hooks() and this lock acquisition. - // The initial call from NativeSocketSampler::start() always has _socket_size == 0 - // and must proceed regardless of _socket_active. - if (_socket_size > 0 && !_socket_active.load(std::memory_order_relaxed)) { - return false; - } - // Only assign orig pointers on the first call (no hooks installed yet). - // On re-entry via dlopen, RTLD_NEXT would resolve to the hook itself. - if (_socket_size == 0) { - NativeSocketSampler::setOriginalFunctions(pre_send, pre_recv, pre_write, pre_read); - } - // TODO: hook table (name + hook fn) should be owned by NativeSocketSampler; - // LibraryPatcher should iterate an externally-provided table rather than - // hardcoding the four socket hooks here. - auto try_patch_slot = [&](void** location, void* hook_fn, const char* fn_name, CodeCache* lib) { - if (location == nullptr) return; - for (int i = 0; i < _socket_size; i++) { - if (_socket_entries[i]._location == location) return; + char* resolved_path = realpath(lib->name(), path); + if (resolved_path == nullptr) { + continue; } - if (_socket_size < 4 * MAX_NATIVE_LIBS) { - void* orig = (void*)__atomic_load_n(location, __ATOMIC_ACQUIRE); - _socket_entries[_socket_size]._lib = lib; - _socket_entries[_socket_size]._location = location; - _socket_entries[_socket_size]._func = orig; - __atomic_store_n(location, hook_fn, __ATOMIC_RELEASE); - _socket_size++; - } else { - Log::warn("socket patch table full (%d slots), skipping %s in %s", 4 * MAX_NATIVE_LIBS, fn_name, lib ? lib->name() : "?"); + SocketPatchTarget target = + socket_patch_target(lib, resolved_path, jdk_library_directory); + if (target == SOCKET_PATCH_NONE) { + continue; + } + if (_profiler_name != nullptr && + strcmp(resolved_path, _profiler_name) == 0) { + continue; } - }; - for (int index = 0; index < capped; index++) { - CodeCache* lib = native_libs.at(index); - if (lib == nullptr) continue; - if (lib->name() == nullptr) continue; - if (is_self[index]) { + size_t patch_count = 0; + for (int hook_index = 0; hook_index < NativeSocketInterposer::NUM_NATIVE_IO_HOOKS; + hook_index++) { + ImportId import_id = hooks[hook_index].import_id; + size_t count = lib->importCount(import_id); + if (count == 0) { + continue; + } + if (!lib->importsComplete(import_id)) { + Log::warn("native I/O hooks disabled: incomplete %s imports in %s", + hooks[hook_index].name, lib->name()); + return disable_and_unpatch(); + } + if (cached_originals[hook_index] == nullptr) { + Log::warn("native I/O hooks disabled: no original for imported %s in %s", + hooks[hook_index].name, lib->name()); + return disable_and_unpatch(); + } + patch_count += count; + } + if (patch_count == 0 || !mappingMatches(lib)) { continue; } - void** send_location = (void**)lib->findImport(im_send); - void** recv_location = (void**)lib->findImport(im_recv); - void** write_location = (void**)lib->findImport(im_write); - void** read_location = (void**)lib->findImport(im_read); + UnloadProtection protection(lib); + if (!protection.isValid()) { + if (!mappingMatches(lib)) { + continue; + } + Log::warn("native I/O hooks disabled: cannot retain mapped DSO %s", + lib->name()); + return disable_and_unpatch(); + } + try { + candidates.emplace_back(lib, std::move(protection), patch_count, target); + } catch (const std::bad_alloc&) { + Log::warn("native I/O hooks disabled: unable to retain DSO candidate %s", + lib->name()); + return disable_and_unpatch(); + } + } - if (send_location == nullptr && recv_location == nullptr - && write_location == nullptr && read_location == nullptr) continue; + std::vector libraries_to_release; + bool success = true; + size_t standard_slots_patched = 0; + size_t ibm_bridge_slots_patched = 0; + { + ExclusiveLockGuard locker(&_lock); + if (require_active && + !_socket_active.load(std::memory_order_relaxed)) { + return false; + } + + size_t additional_patches = 0; + size_t additional_libraries = 0; + for (SocketPatchCandidate& candidate : candidates) { + if (!socket_library_patched_unlocked(candidate._lib->imageBase())) { + additional_patches += candidate._patch_count; + additional_libraries++; + } + } + + try { + _socket_entries.reserve(_socket_entries.size() + additional_patches); + _socket_libraries.reserve(_socket_libraries.size() + additional_libraries); + } catch (const std::bad_alloc&) { + Log::warn("native I/O hooks disabled: unable to reserve patch transaction"); + success = false; + } + + if (success) { + for (SocketPatchCandidate& candidate : candidates) { + if (!socket_library_patched_unlocked(candidate._lib->imageBase()) && + !candidate._lib->prepareImportsForPatch()) { + Log::warn("native I/O hooks disabled: cannot make imports writable in %s", + candidate._lib->name()); + success = false; + break; + } + } + } - TEST_LOG("patch_socket_functions PATCH %s send=%p recv=%p write=%p read=%p", - lib->name(), (void*)send_location, (void*)recv_location, - (void*)write_location, (void*)read_location); + if (success) { + for (SocketPatchCandidate& candidate : candidates) { + CodeCache* lib = candidate._lib; + if (socket_library_patched_unlocked(lib->imageBase())) { + continue; + } + size_t first_patch = _socket_entries.size(); + for (int hook_index = 0; + hook_index < NativeSocketInterposer::NUM_NATIVE_IO_HOOKS; + hook_index++) { + ImportId import_id = hooks[hook_index].import_id; + size_t count = lib->importCount(import_id); + for (size_t import_index = 0; import_index < count; import_index++) { + void** location = lib->findImport(import_id, import_index); + void* original = + reinterpret_cast(__atomic_load_n(location, __ATOMIC_ACQUIRE)); + _socket_entries.push_back({location, original}); + void* hook = socket_hook_for_target(candidate._target, + hooks[hook_index]); + __atomic_store_n(location, hook, __ATOMIC_RELEASE); + if (candidate._target == SOCKET_PATCH_IBM_JCL_BRIDGE) { + ibm_bridge_slots_patched++; + } else { + standard_slots_patched++; + } + } + } + _socket_libraries.push_back( + {lib->imageBase(), candidate._protection.release(), first_patch, + _socket_entries.size() - first_patch}); + remember_socket_library_unlocked(lib->imageBase()); + } + _socket_active.store(true, std::memory_order_release); + } else { + unpatch_socket_functions_unlocked(libraries_to_release); + } + } - // The _lock is held during patching to protect _socket_entries and _socket_size. - // Concurrent dlopen_hook calls serialize via the same lock in install_socket_hooks(), - // ensuring slot_patched checks and updates are atomic with respect to each other. - try_patch_slot(send_location, (void*)NativeSocketSampler::send_hook, "send", lib); - try_patch_slot(recv_location, (void*)NativeSocketSampler::recv_hook, "recv", lib); - try_patch_slot(write_location, (void*)NativeSocketSampler::write_hook, "write", lib); - try_patch_slot(read_location, (void*)NativeSocketSampler::read_hook, "read", lib); + release_socket_libraries(libraries_to_release); + if (!success) { + NativeSocketInterposer::instance()->disableAfterPatchFailure(); + NativeSocketSampler::disableAfterPatchFailure(); + return false; } - TEST_LOG("patch_socket_functions DONE total_slots=%d num_libs_scanned=%d", - _socket_size, capped); - _socket_active.store(true, std::memory_order_release); + Counters::increment(NATIVE_IO_STANDARD_HOOKS_PATCHED, + standard_slots_patched); + Counters::increment(NATIVE_IO_IBM_BRIDGE_HOOKS_PATCHED, + ibm_bridge_slots_patched); + TEST_LOG("patch_socket_functions DONE total_slots=%zu standard_new=%zu " + "ibm_bridge_new=%zu num_libs_scanned=%d", + _socket_entries.size(), standard_slots_patched, + ibm_bridge_slots_patched, capped); return true; } -void LibraryPatcher::unpatch_socket_functions() { +#ifdef UNIT_TEST +int LibraryPatcher::patch_socket_import_for_test(CodeCache* lib, ImportId import_id, + void* hook, const char* name, + bool retain_library) { + (void)name; + UnloadProtection protection(lib); + if (retain_library && + (!mappingMatches(lib) || !protection.isValid())) { + return -1; + } + ExclusiveLockGuard locker(&_lock); + if (!lib->importsComplete(import_id) || !lib->prepareImportsForPatch()) { + return -1; + } + size_t count = lib->importCount(import_id); + size_t initial_size = _socket_entries.size(); + try { + _socket_entries.reserve(initial_size + count); + if (retain_library) { + _socket_libraries.reserve(_socket_libraries.size() + 1); + } + } catch (const std::bad_alloc&) { + return -1; + } + for (size_t index = 0; index < count; index++) { + void** location = lib->findImport(import_id, index); + bool already_patched = std::any_of( + _socket_entries.begin(), _socket_entries.end(), + [location](const SocketPatchEntry& entry) { + return entry._location == location; + }); + if (already_patched) { + continue; + } + void* original = + reinterpret_cast(__atomic_load_n(location, __ATOMIC_ACQUIRE)); + _socket_entries.push_back({location, original}); + __atomic_store_n(location, hook, __ATOMIC_RELEASE); + } + size_t patched = _socket_entries.size() - initial_size; + if (retain_library && patched != 0) { + _socket_libraries.push_back( + {lib->imageBase(), protection.release(), initial_size, patched}); + remember_socket_library_unlocked(lib->imageBase()); + } + return static_cast(patched); +} + +int LibraryPatcher::socket_patch_count_for_test() { ExclusiveLockGuard locker(&_lock); + return static_cast(_socket_entries.size()); +} + +int LibraryPatcher::socket_library_count_for_test() { + ExclusiveLockGuard locker(&_lock); + return static_cast(_socket_libraries.size()); +} + +SocketPatchTarget LibraryPatcher::socket_patch_target_for_test( + CodeCache* lib, const char* library_name, bool in_jdk_directory) { + return socket_patch_target_for_library(lib, library_name, in_jdk_directory); +} + +void* LibraryPatcher::socket_hook_for_target_for_test( + SocketPatchTarget target, int hook_index) { + if (hook_index < 0 || + hook_index >= NativeSocketInterposer::NUM_NATIVE_IO_HOOKS) { + return nullptr; + } + return socket_hook_for_target(target, + NativeSocketInterposer::hookSpecs()[hook_index]); +} +#endif + +void LibraryPatcher::unpatch_socket_functions_unlocked( + std::vector& libraries_to_release) { // Clear _socket_active FIRST so that any concurrent install_socket_hooks() // thread that already passed the acquire-load on _socket_active (before we // acquired the lock) will see false when it checks again after acquiring the // lock — preventing it from re-patching slots we are about to restore. // Hooks that already entered the hook body before this store are benign: they // hold no lock and will complete normally using the still-valid orig pointers. - // - // ASSUMPTION (dlclose UAF): we write through _socket_entries[i]._location - // without checking that the owning library is still mapped. If a patched - // DSO were actually unmapped between patch and unpatch, this store would - // corrupt freed memory or SEGV. In practice this is benign because (a) the - // host JVM does not dlclose libc-importing DSOs, (b) glibc's dlclose - // refcounts and only unmaps when the final reference is dropped, and - // (c) the same risk is already accepted by unpatch_libraries() and - // unpatch_socket_functions has the same trust model. If a host that - // routinely unmaps libc-importing libraries is ever supported, gate each - // store on a /proc/self/maps lookup or hold a dlopen handle on each lib - // for the patch lifetime. _socket_active.store(false, std::memory_order_release); - TEST_LOG("unpatch_socket_functions restoring %d slot(s)", _socket_size); - for (int index = 0; index < _socket_size; index++) { - __atomic_store_n(_socket_entries[index]._location, _socket_entries[index]._func, __ATOMIC_RELEASE); + TEST_LOG("unpatch_socket_functions restoring %zu slot(s)", _socket_entries.size()); + for (const SocketPatchEntry& entry : _socket_entries) { + __atomic_store_n(entry._location, entry._func, + __ATOMIC_RELEASE); } - _socket_size = 0; - // _orig_send/_orig_recv/_orig_write/_orig_read are intentionally NOT nulled. + _socket_entries.clear(); + memset(_socket_bases, 0, sizeof(_socket_bases)); + _socket_libraries.swap(libraries_to_release); + // Original function pointers are intentionally NOT nulled. // In-flight hook invocations that entered before PLT entries were restored // above may still be executing and will dereference these pointers. // They remain valid (pointing to the real libc functions) until the next // patch_socket_functions() call. } +void LibraryPatcher::release_socket_libraries( + std::vector& libraries) { + for (const SocketPatchedLibrary& library : libraries) { + if (library._unload_protection != nullptr) { + dlclose(library._unload_protection); + } + } + libraries.clear(); +} + +void LibraryPatcher::unpatch_socket_functions() { + std::vector libraries_to_release; + { + ExclusiveLockGuard locker(&_lock); + unpatch_socket_functions_unlocked(libraries_to_release); + } + release_socket_libraries(libraries_to_release); +} + +bool LibraryPatcher::unpatch_socket_functions_if_inactive() { + std::vector libraries_to_release; + { + ExclusiveLockGuard locker(&_lock); + if (NativeSocketInterposer::instance()->active() || NativeSocketSampler::active()) { + return false; + } + if (!_socket_active.load(std::memory_order_relaxed) && + _socket_entries.empty()) { + return false; + } + unpatch_socket_functions_unlocked(libraries_to_release); + } + release_socket_libraries(libraries_to_release); + return true; +} + +void LibraryPatcher::install_socket_hooks() { + if (_socket_active.load(std::memory_order_acquire) && + !patch_socket_functions(true)) { + NativeSocketInterposer::instance()->disableAfterPatchFailure(); + NativeSocketSampler::disableAfterPatchFailure(); + } +} + #endif // __linux__ diff --git a/ddprof-lib/src/main/cpp/nativeBlock.cpp b/ddprof-lib/src/main/cpp/nativeBlock.cpp new file mode 100644 index 0000000000..ad5a6fdd51 --- /dev/null +++ b/ddprof-lib/src/main/cpp/nativeBlock.cpp @@ -0,0 +1,133 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "nativeBlock.h" + +#if defined(__linux__) + +#include "context_api.h" +#include "profiler.h" +#include "taskBlockRecorder.h" +#include "threadLocalData.h" +#include "tsc.h" + +#include +#include + +#ifdef UNIT_TEST +static std::atomic _native_block_observer{nullptr}; + +void NativeBlockScope::setHookObserverForTest(HookObserver observer) { + _native_block_observer.store(observer, std::memory_order_release); +} + +static void observeNativeBlockPhase(const char* phase, NativeBlockKind kind, int blocker_id) { + NativeBlockScope::HookObserver observer = + _native_block_observer.load(std::memory_order_acquire); + if (observer != nullptr) { + observer(phase, kind, blocker_id); + } +} +#endif + +NativeBlockScope::NativeBlockScope(NativeBlockKind kind, int blocker_id, + OSThreadState state) + : _blocker(blocker(kind, blocker_id)), _state(state) { + int saved_errno = errno; +#ifdef UNIT_TEST + observeNativeBlockPhase("enter", kind, blocker_id); +#endif + + Profiler* profiler = Profiler::instance(); + if (!profiler->taskBlockEnabled()) { + errno = saved_errno; + return; + } + + ThreadFilter* thread_filter = profiler->threadFilter(); + if (!thread_filter->registryActive()) { + errno = saved_errno; + return; + } + + ProfiledThread* current = ProfiledThread::current(); + if (current == nullptr || current->threadType() != ProfiledThread::TYPE_JAVA_THREAD) { + errno = saved_errno; + return; + } + + ThreadFilter::SlotID slot_id = current->filterSlotId(); + if (slot_id < 0) { + errno = saved_errno; + return; + } + + Context context = ContextApi::snapshot(); + if (context.spanId != 0) { + Counters::increment(TASK_BLOCK_SKIPPED_TRACE_CONTEXT); + errno = saved_errno; + return; + } + + u64 token = thread_filter->enterBlockedRun(slot_id, state, BlockRunOwner::NATIVE); + if (token == 0) { + errno = saved_errno; + return; + } + + _active = true; + _tid = current->tid(); + _slot_id = slot_id; + _generation = ThreadFilter::tokenGeneration(token); + _start_ticks = TSC::ticks(); + _context = context; + errno = saved_errno; +} + +NativeBlockScope::~NativeBlockScope() { +#ifdef UNIT_TEST + observeNativeBlockPhase("exit", static_cast(_blocker >> 32), + static_cast(_blocker & 0xffffffff)); +#endif + if (!_active) { + return; + } + int saved_errno = errno; + finish(TSC::ticks()); + errno = saved_errno; +} + +void NativeBlockScope::finish(u64 end_ticks) { + if (!_active) { + return; + } + _active = false; + + Profiler* profiler = Profiler::instance(); + ThreadFilter* thread_filter = profiler->threadFilter(); + bool recording_enabled = + profiler->taskBlockEnabled() && thread_filter->registryActive(); + bool activity = profiler->tryEnterTaskBlockActivity(); + BlockRunSnapshot snapshot{}; + bool exited = thread_filter->snapshotAndExitBlockedRun( + _slot_id, _generation, &snapshot); + + if (!activity) { + Counters::increment(TASK_BLOCK_DROPPED_ROTATION); + return; + } + + if (!recording_enabled || !exited) { + profiler->leaveTaskBlockActivity(); + return; + } + + recordTaskBlockIfEligible(_tid, nullptr, 0, _start_ticks, end_ticks, + _context, _blocker, 0, + snapshot.active_state, true); + profiler->leaveTaskBlockActivity(); +} + +#endif // __linux__ diff --git a/ddprof-lib/src/main/cpp/nativeBlock.h b/ddprof-lib/src/main/cpp/nativeBlock.h new file mode 100644 index 0000000000..565bbe0365 --- /dev/null +++ b/ddprof-lib/src/main/cpp/nativeBlock.h @@ -0,0 +1,66 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _NATIVE_BLOCK_H +#define _NATIVE_BLOCK_H + +#include "arch.h" + +#if defined(__linux__) + +#include "context.h" +#include "threadFilter.h" +#include "threadState.h" + +enum class NativeBlockKind : u32 { + STREAM_SOCKET = 1, + CONNECT = 2, + ACCEPT = 3, + UDP_RECEIVE = 4, + POLL = 5, + SELECT = 6, + EPOLL_WAIT = 7, +}; + +// Describes physical blocking by the current OS thread. When called by a virtual +// thread in JNI, the recorded thread is the pinned carrier, not the logical thread. +class NativeBlockScope { +public: + NativeBlockScope(NativeBlockKind kind, int blocker_id, + OSThreadState state = OSThreadState::IO_WAIT); + ~NativeBlockScope(); + + NativeBlockScope(const NativeBlockScope&) = delete; + NativeBlockScope& operator=(const NativeBlockScope&) = delete; + + bool active() const { return _active; } + + static u64 blocker(NativeBlockKind kind, int blocker_id) { + return (static_cast(kind) << 32) | static_cast(blocker_id); + } + +#ifdef UNIT_TEST + using HookObserver = void (*)(const char* phase, NativeBlockKind kind, int blocker_id); + static void setHookObserverForTest(HookObserver observer); + u64 startTicksForTest() const { return _start_ticks; } + void finishForTest(u64 end_ticks) { finish(end_ticks); } +#endif + +private: + bool _active = false; + int _tid = -1; + ThreadFilter::SlotID _slot_id = -1; + u64 _generation = 0; + u64 _start_ticks = 0; + u64 _blocker = 0; + OSThreadState _state = OSThreadState::UNKNOWN; + Context _context = {}; + + void finish(u64 end_ticks); +}; + +#endif // __linux__ + +#endif // _NATIVE_BLOCK_H diff --git a/ddprof-lib/src/main/cpp/nativeFdClassifier.cpp b/ddprof-lib/src/main/cpp/nativeFdClassifier.cpp new file mode 100644 index 0000000000..43b17bdd41 --- /dev/null +++ b/ddprof-lib/src/main/cpp/nativeFdClassifier.cpp @@ -0,0 +1,289 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "nativeFdClassifier.h" + +#if defined(__linux__) + +#include +#include +#include + +#ifdef UNIT_TEST +std::atomic NativeFdClassifier::_probe_override{nullptr}; +std::atomic NativeFdClassifier::_probe_count{0}; +#endif + +NativeFdClassifier::NativeFdClassifier() { + for (int index = 0; index < FD_TYPE_CACHE_SIZE; index++) { + _fd_type_cache[index].store(0, std::memory_order_relaxed); + } + for (int index = 0; index < HIGH_FD_TYPE_CACHE_SIZE; index++) { + _high_fd_type_cache[index].store(0, std::memory_order_relaxed); + } +} + +#ifdef UNIT_TEST +void NativeFdClassifier::setProbeOverrideForTest(ProbeOverride probe) { + _probe_override.store(probe, std::memory_order_release); +} + +uint64_t NativeFdClassifier::probeCountForTest() { + return _probe_count.load(std::memory_order_acquire); +} + +void NativeFdClassifier::resetProbeCountForTest() { + _probe_count.store(0, std::memory_order_release); +} +#endif + +uint8_t NativeFdClassifier::probeFdType(int fd) { +#ifdef UNIT_TEST + _probe_count.fetch_add(1, std::memory_order_relaxed); +#endif + int so_type; + socklen_t solen = sizeof(so_type); + int rc; +#ifdef UNIT_TEST + ProbeOverride probe = _probe_override.load(std::memory_order_acquire); + int probe_errno = 0; + if (probe != nullptr) { + rc = probe(fd, &so_type, &probe_errno); + if (rc != 0) { + errno = probe_errno; + } + } else +#endif + { + rc = getsockopt(fd, SOL_SOCKET, SO_TYPE, &so_type, &solen); + } + if (rc == 0) { + if (so_type == SOCK_STREAM) { + return FD_TYPE_STREAM_SOCKET; + } + if (so_type == SOCK_DGRAM) { + return FD_TYPE_DATAGRAM_SOCKET; + } + return FD_TYPE_OTHER_SOCKET; + } + return errno == ENOTSOCK ? FD_TYPE_NON_SOCKET : 0; +} + +uint64_t NativeFdClassifier::fdEntry(uint32_t epoch, uint64_t incarnation, + uint8_t type) { + return (static_cast(epoch) << FD_TYPE_EPOCH_SHIFT) + | ((incarnation & FD_TYPE_INCARNATION_MASK) + << FD_TYPE_INCARNATION_SHIFT) + | static_cast(type); +} + +uint32_t NativeFdClassifier::fdEntryEpoch(uint64_t entry) { + return static_cast(entry >> FD_TYPE_EPOCH_SHIFT); +} + +uint64_t NativeFdClassifier::fdEntryIncarnation(uint64_t entry) { + return (entry >> FD_TYPE_INCARNATION_SHIFT) & FD_TYPE_INCARNATION_MASK; +} + +void NativeFdClassifier::cacheFdType(int fd, uint8_t type) { + if (fd < 0 || type == 0) { + return; + } + if (static_cast(fd) < static_cast(FD_TYPE_CACHE_SIZE)) { + uint32_t epoch = _fd_cache_gen.load(std::memory_order_acquire); + uint64_t cached = _fd_type_cache[fd].load(std::memory_order_acquire); + uint64_t desired = fdEntry(epoch, fdEntryIncarnation(cached), type); + _fd_type_cache[fd].compare_exchange_strong( + cached, desired, std::memory_order_acq_rel, std::memory_order_acquire); + } else { + uint32_t epoch = _fd_cache_gen.load(std::memory_order_acquire); + int index = highFdCacheIndex(fd); + uint64_t cached = _high_fd_type_cache[index].load(std::memory_order_acquire); + uint64_t desired = highFdEntry(fd, epoch, highFdEntryIncarnation(cached), + type); + _high_fd_type_cache[index].compare_exchange_strong( + cached, desired, std::memory_order_acq_rel, std::memory_order_acquire); + } +} + +uint64_t NativeFdClassifier::highFdEntry(int fd, uint32_t epoch, + uint64_t incarnation, uint8_t type) { + // The direct-mapped index carries the low 12 fd bits. The stored quotient + // carries the remaining 19 bits of a non-negative int fd, leaving room for + // a 17-bit cache epoch and a 24-bit slot incarnation in one atomic word. + uint64_t fd_tag = static_cast(fd) / + static_cast(HIGH_FD_TYPE_CACHE_SIZE); + return (fd_tag << HIGH_FD_TAG_SHIFT) + | (static_cast(epoch & HIGH_FD_EPOCH_MASK) + << HIGH_FD_EPOCH_SHIFT) + | ((incarnation & HIGH_FD_INCARNATION_MASK) + << FD_TYPE_INCARNATION_SHIFT) + | static_cast(type); +} + +uint32_t NativeFdClassifier::highFdEntryEpoch(uint64_t entry) { + return static_cast((entry >> HIGH_FD_EPOCH_SHIFT) + & HIGH_FD_EPOCH_MASK); +} + +uint64_t NativeFdClassifier::highFdEntryIncarnation(uint64_t entry) { + return (entry >> FD_TYPE_INCARNATION_SHIFT) & HIGH_FD_INCARNATION_MASK; +} + +bool NativeFdClassifier::highFdEntryMatches(uint64_t entry, int fd, + uint32_t epoch) { + return highFdEntryMatchesFd(entry, fd) + && highFdEntryEpoch(entry) == (epoch & HIGH_FD_EPOCH_MASK); +} + +bool NativeFdClassifier::highFdEntryMatchesFd(uint64_t entry, int fd) { + uint64_t fd_tag = static_cast(fd) / + static_cast(HIGH_FD_TYPE_CACHE_SIZE); + return (entry >> HIGH_FD_TAG_SHIFT) == fd_tag; +} + +int NativeFdClassifier::highFdCacheIndex(int fd) { + return static_cast(static_cast(fd) % + static_cast(HIGH_FD_TYPE_CACHE_SIZE)); +} + +uint8_t NativeFdClassifier::highFdType(int fd) { + uint32_t epoch = _fd_cache_gen.load(std::memory_order_acquire); + int index = highFdCacheIndex(fd); + uint64_t cached = _high_fd_type_cache[index].load(std::memory_order_acquire); + if (highFdEntryMatches(cached, fd, epoch)) { + uint8_t type = static_cast(cached & FD_TYPE_MASK); + if (type != 0) { + return type; + } + } + + uint8_t type = probeFdType(fd); + // probeFdType() returns 0 for transient errors such as EBADF. Do not cache those: + // the same fd number may later be reused for a socket. + if (type != 0) { + uint64_t desired = highFdEntry(fd, epoch, + highFdEntryIncarnation(cached), type); + if (_high_fd_type_cache[index].compare_exchange_strong( + cached, desired, std::memory_order_acq_rel, + std::memory_order_acquire)) { + return (_fd_cache_gen.load(std::memory_order_acquire) + & HIGH_FD_EPOCH_MASK) == (epoch & HIGH_FD_EPOCH_MASK) + ? type : 0; + } + uint32_t current_epoch = _fd_cache_gen.load(std::memory_order_acquire); + if (highFdEntryMatches(cached, fd, current_epoch)) { + return static_cast(cached & FD_TYPE_MASK); + } + return 0; + } + return type; +} + +uint8_t NativeFdClassifier::fdType(int fd) { + if (fd < 0) { + return 0; + } + + if (static_cast(fd) >= static_cast(FD_TYPE_CACHE_SIZE)) { + return highFdType(fd); + } + + uint32_t epoch = _fd_cache_gen.load(std::memory_order_acquire); + uint64_t cached = _fd_type_cache[fd].load(std::memory_order_acquire); + if (fdEntryEpoch(cached) == epoch) { + uint8_t type = static_cast(cached & FD_TYPE_MASK); + if (type != 0) { + return type; + } + } + + uint8_t type = probeFdType(fd); + // probeFdType() returns 0 for transient errors such as EBADF. Do not cache those: + // the same fd number may later be reused for a socket. + if (type != 0) { + uint64_t desired = fdEntry(epoch, fdEntryIncarnation(cached), type); + if (_fd_type_cache[fd].compare_exchange_strong( + cached, desired, std::memory_order_acq_rel, + std::memory_order_acquire)) { + return _fd_cache_gen.load(std::memory_order_acquire) == epoch ? type : 0; + } + uint32_t current_epoch = _fd_cache_gen.load(std::memory_order_acquire); + if (fdEntryEpoch(cached) == current_epoch) { + return static_cast(cached & FD_TYPE_MASK); + } + return 0; + } + return type; +} + +bool NativeFdClassifier::isStreamSocket(int fd) { + return fdType(fd) == FD_TYPE_STREAM_SOCKET; +} + +bool NativeFdClassifier::isDatagramSocket(int fd) { + return fdType(fd) == FD_TYPE_DATAGRAM_SOCKET; +} + +void NativeFdClassifier::cacheNonSocket(int fd) { + if (fd < 0) { + return; + } + cacheFdType(fd, FD_TYPE_NON_SOCKET); +} + +void NativeFdClassifier::clearHighFdType(int fd) { + int index = highFdCacheIndex(fd); + uint32_t epoch = _fd_cache_gen.load(std::memory_order_acquire); + uint64_t cached = _high_fd_type_cache[index].load(std::memory_order_acquire); + for (;;) { + // Advance the direct-mapped slot even when it currently holds a colliding + // fd. This makes every probe that started before this lifecycle event lose + // its publication CAS without unnecessarily evicting the colliding type. + uint64_t incarnation = highFdEntryIncarnation(cached) + 1; + bool current_entry = highFdEntryEpoch(cached) == + (epoch & HIGH_FD_EPOCH_MASK); + int cached_fd = current_entry + ? static_cast((cached >> HIGH_FD_TAG_SHIFT) * + HIGH_FD_TYPE_CACHE_SIZE + index) + : fd; + uint8_t type = current_entry && cached_fd != fd + ? static_cast(cached & FD_TYPE_MASK) : 0; + uint64_t desired = highFdEntry(cached_fd, epoch, incarnation, type); + if (_high_fd_type_cache[index].compare_exchange_weak( + cached, desired, std::memory_order_acq_rel, + std::memory_order_acquire)) { + return; + } + epoch = _fd_cache_gen.load(std::memory_order_acquire); + } +} + +void NativeFdClassifier::clearFdType(int fd) { + if (fd < 0) { + return; + } + if (static_cast(fd) < static_cast(FD_TYPE_CACHE_SIZE)) { + uint32_t epoch = _fd_cache_gen.load(std::memory_order_acquire); + uint64_t cached = _fd_type_cache[fd].load(std::memory_order_acquire); + for (;;) { + uint64_t desired = fdEntry(epoch, fdEntryIncarnation(cached) + 1, 0); + if (_fd_type_cache[fd].compare_exchange_weak( + cached, desired, std::memory_order_acq_rel, + std::memory_order_acquire)) { + return; + } + epoch = _fd_cache_gen.load(std::memory_order_acquire); + } + } else { + clearHighFdType(fd); + } +} + +void NativeFdClassifier::clearFdTypeCache() { + _fd_cache_gen.fetch_add(1, std::memory_order_acq_rel); +} + +#endif // __linux__ diff --git a/ddprof-lib/src/main/cpp/nativeFdClassifier.h b/ddprof-lib/src/main/cpp/nativeFdClassifier.h new file mode 100644 index 0000000000..cdcafd2986 --- /dev/null +++ b/ddprof-lib/src/main/cpp/nativeFdClassifier.h @@ -0,0 +1,78 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _NATIVE_FD_CLASSIFIER_H +#define _NATIVE_FD_CLASSIFIER_H + +#include +#include + +#if defined(__linux__) + +class NativeFdClassifier { +public: + NativeFdClassifier(); + + bool isStreamSocket(int fd); + bool isDatagramSocket(int fd); + void cacheNonSocket(int fd); + void clearFdType(int fd); + void clearFdTypeCache(); + +#ifdef UNIT_TEST + using ProbeOverride = int (*)(int fd, int *so_type, int *probe_errno); + static void setProbeOverrideForTest(ProbeOverride probe); + static uint64_t probeCountForTest(); + static void resetProbeCountForTest(); +#endif + +private: + static const int FD_TYPE_CACHE_SIZE = 65536; + static const int HIGH_FD_TYPE_CACHE_SIZE = 4096; + static const uint64_t FD_TYPE_MASK = 0xf; + static const int FD_TYPE_INCARNATION_SHIFT = 4; + static const uint64_t FD_TYPE_INCARNATION_MASK = 0x0fffffff; + static const int FD_TYPE_EPOCH_SHIFT = 32; + static const uint64_t HIGH_FD_INCARNATION_MASK = 0x00ffffff; + static const int HIGH_FD_EPOCH_SHIFT = 28; + static const uint64_t HIGH_FD_EPOCH_MASK = 0x1ffff; + static const int HIGH_FD_TAG_SHIFT = 45; + static const uint8_t FD_TYPE_STREAM_SOCKET = 1; + static const uint8_t FD_TYPE_DATAGRAM_SOCKET = 2; + static const uint8_t FD_TYPE_OTHER_SOCKET = 3; + static const uint8_t FD_TYPE_NON_SOCKET = 4; + + // A low-fd entry atomically couples its cached type to both the profiler + // cache epoch and that fd's lifecycle incarnation. A probe may publish only + // if close/dup has not changed the entry it observed before getsockopt(). + std::atomic _fd_cache_gen{1}; + std::atomic _fd_type_cache[FD_TYPE_CACHE_SIZE]; + std::atomic _high_fd_type_cache[HIGH_FD_TYPE_CACHE_SIZE]; + + static uint8_t probeFdType(int fd); + static uint64_t fdEntry(uint32_t epoch, uint64_t incarnation, uint8_t type); + static uint32_t fdEntryEpoch(uint64_t entry); + static uint64_t fdEntryIncarnation(uint64_t entry); + static uint64_t highFdEntry(int fd, uint32_t epoch, uint64_t incarnation, + uint8_t type); + static uint32_t highFdEntryEpoch(uint64_t entry); + static uint64_t highFdEntryIncarnation(uint64_t entry); + static bool highFdEntryMatches(uint64_t entry, int fd, uint32_t gen); + static bool highFdEntryMatchesFd(uint64_t entry, int fd); + static int highFdCacheIndex(int fd); + void cacheFdType(int fd, uint8_t type); + uint8_t highFdType(int fd); + void clearHighFdType(int fd); + uint8_t fdType(int fd); + +#ifdef UNIT_TEST + static std::atomic _probe_override; + static std::atomic _probe_count; +#endif +}; + +#endif // __linux__ + +#endif // _NATIVE_FD_CLASSIFIER_H diff --git a/ddprof-lib/src/main/cpp/nativeSocketInterposer.cpp b/ddprof-lib/src/main/cpp/nativeSocketInterposer.cpp new file mode 100644 index 0000000000..c674fd49dd --- /dev/null +++ b/ddprof-lib/src/main/cpp/nativeSocketInterposer.cpp @@ -0,0 +1,787 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "nativeSocketInterposer.h" + +#if defined(__linux__) + +#include "counters.h" +#include "libraries.h" +#include "libraryPatcher.h" +#include "log.h" +#include "nativeSocketSampler.h" +#include "tsc.h" + +#include +#include +#include + +static inline bool nonZeroTimeval(const struct timeval* timeout) { + return timeout == nullptr || timeout->tv_sec != 0 || timeout->tv_usec != 0; +} + +static inline bool nonZeroTimespec(const struct timespec* timeout) { + return timeout == nullptr || timeout->tv_sec != 0 || timeout->tv_nsec != 0; +} + +class ErrnoGuard { +public: + ErrnoGuard() : _saved(errno) {} + ~ErrnoGuard() { errno = _saved; } + +private: + int _saved; +}; + +template +static inline Ret runNativeIoHook(bool eligible, NativeBlockKind kind, int fd, + Fn fn, Call call) { + if (fn == nullptr) { + errno = ENOSYS; + return static_cast(-1); + } + if (!NativeSocketInterposer::instance()->active() || !eligible) { + return call(fn); + } + + NativeBlockScope block(kind, fd); + Ret ret = call(fn); + return ret; +} + +template +static inline ssize_t runStreamSocketHook(int fd, Fn fn, u8 op, Call call) { + if (fn == nullptr) { + errno = ENOSYS; + return -1; + } + + // read/write are intentionally routed through the socket classifier because + // socket I/O often uses generic fd APIs. Non-socket fds are cached after the + // first stable ENOTSOCK probe; high or transiently failing fds may be probed + // again on later calls. + bool eligible; + { + ErrnoGuard errno_guard; + eligible = NativeSocketInterposer::instance()->isStreamSocket(fd); + } + if (!eligible) { + return call(fn); + } + + u64 t0 = TSC::ticks(); + ssize_t ret; + { + NativeBlockScope block(NativeBlockKind::STREAM_SOCKET, fd); + ret = call(fn); + } + u64 t1 = TSC::ticks(); + if (NativeSocketSampler::active()) { + ErrnoGuard errno_guard; + return NativeSocketSampler::recordHookResult(fd, ret, t0, t1, op); + } + return ret; +} + +template +static inline Ret runDatagramSocketHook(int fd, Fn fn, Call call) { + bool eligible; + { + ErrnoGuard errno_guard; + eligible = NativeSocketInterposer::instance()->isDatagramSocket(fd); + } + return runNativeIoHook(eligible, NativeBlockKind::UDP_RECEIVE, fd, fn, + call); +} + +NativeSocketInterposer* const NativeSocketInterposer::_instance = new NativeSocketInterposer(); +std::atomic NativeSocketInterposer::_orig_send{nullptr}; +std::atomic NativeSocketInterposer::_orig_recv{nullptr}; +std::atomic NativeSocketInterposer::_orig_write{nullptr}; +std::atomic NativeSocketInterposer::_orig_read{nullptr}; +std::atomic NativeSocketInterposer::_orig_close{nullptr}; +std::atomic NativeSocketInterposer::_orig_dup2{nullptr}; +std::atomic NativeSocketInterposer::_orig_dup3{nullptr}; +std::atomic NativeSocketInterposer::_orig_connect{nullptr}; +std::atomic NativeSocketInterposer::_orig_accept{nullptr}; +std::atomic NativeSocketInterposer::_orig_accept4{nullptr}; +std::atomic NativeSocketInterposer::_orig_recvfrom{nullptr}; +std::atomic NativeSocketInterposer::_orig_recvmsg{nullptr}; +std::atomic NativeSocketInterposer::_orig_epoll_wait{nullptr}; +std::atomic NativeSocketInterposer::_orig_epoll_pwait{nullptr}; +std::atomic NativeSocketInterposer::_orig_poll{nullptr}; +std::atomic NativeSocketInterposer::_orig_ppoll{nullptr}; +std::atomic NativeSocketInterposer::_orig_select{nullptr}; +std::atomic NativeSocketInterposer::_orig_pselect{nullptr}; +std::atomic NativeSocketInterposer::_hook_owner_pid{0}; + +static_assert(std::atomic::is_always_lock_free, + "native I/O hook function pointers must be lock-free"); +static_assert(std::atomic::is_always_lock_free, + "native I/O hook owner PID must be lock-free"); + +const NativeSocketInterposer::NativeIoHookSpec* NativeSocketInterposer::hookSpecs() { + static const NativeIoHookSpec specs[NUM_NATIVE_IO_HOOKS] = { + {im_send, "send", reinterpret_cast(send_hook), + reinterpret_cast(fork_safe_send_hook)}, + {im_recv, "recv", reinterpret_cast(recv_hook), + reinterpret_cast(fork_safe_recv_hook)}, + {im_write, "write", reinterpret_cast(write_hook), + reinterpret_cast(fork_safe_write_hook)}, + {im_read, "read", reinterpret_cast(read_hook), + reinterpret_cast(fork_safe_read_hook)}, + {im_close, "close", reinterpret_cast(close_hook), + reinterpret_cast(fork_safe_close_hook)}, + {im_dup2, "dup2", reinterpret_cast(dup2_hook), + reinterpret_cast(fork_safe_dup2_hook)}, + {im_dup3, "dup3", reinterpret_cast(dup3_hook), + reinterpret_cast(fork_safe_dup3_hook)}, + {im_connect, "connect", reinterpret_cast(connect_hook), + reinterpret_cast(fork_safe_connect_hook)}, + {im_accept, "accept", reinterpret_cast(accept_hook), + reinterpret_cast(fork_safe_accept_hook)}, + {im_accept4, "accept4", reinterpret_cast(accept4_hook), + reinterpret_cast(fork_safe_accept4_hook)}, + {im_recvfrom, "recvfrom", reinterpret_cast(recvfrom_hook), + reinterpret_cast(fork_safe_recvfrom_hook)}, + {im_recvmsg, "recvmsg", reinterpret_cast(recvmsg_hook), + reinterpret_cast(fork_safe_recvmsg_hook)}, + {im_epoll_wait, "epoll_wait", reinterpret_cast(epoll_wait_hook), + reinterpret_cast(fork_safe_epoll_wait_hook)}, + {im_epoll_pwait, "epoll_pwait", reinterpret_cast(epoll_pwait_hook), + reinterpret_cast(fork_safe_epoll_pwait_hook)}, + {im_poll, "poll", reinterpret_cast(poll_hook), + reinterpret_cast(fork_safe_poll_hook)}, + {im_ppoll, "ppoll", reinterpret_cast(ppoll_hook), + reinterpret_cast(fork_safe_ppoll_hook)}, + {im_select, "select", reinterpret_cast(select_hook), + reinterpret_cast(fork_safe_select_hook)}, + {im_pselect, "pselect", reinterpret_cast(pselect_hook), + reinterpret_cast(fork_safe_pselect_hook)}, + }; + return specs; +} + +static bool markJavaProfilerHook(void* fn_addr) { + CodeCache* lib = Libraries::instance()->findLibraryByAddress(fn_addr); + if (lib == nullptr) { + Counters::increment(NATIVE_HOOK_MARK_RESOLVE_FAILED); + return false; + } + const char* name = nullptr; + lib->binarySearch(fn_addr, &name); + if (name == nullptr) { + Counters::increment(NATIVE_HOOK_MARK_RESOLVE_FAILED); + return false; + } + NativeFunc::set_mark(name, MARK_JAVA_PROFILER); + return true; +} + +bool NativeSocketInterposer::markProfilerHooks() { + bool hooks_marked = + markJavaProfilerHook(reinterpret_cast(NativeSocketSampler::send_hook)); + hooks_marked &= + markJavaProfilerHook(reinterpret_cast(NativeSocketSampler::recv_hook)); + hooks_marked &= + markJavaProfilerHook(reinterpret_cast(NativeSocketSampler::write_hook)); + hooks_marked &= + markJavaProfilerHook(reinterpret_cast(NativeSocketSampler::read_hook)); + + const NativeIoHookSpec* specs = hookSpecs(); + for (int hook_index = HOOK_SEND; hook_index <= HOOK_READ; hook_index++) { + hooks_marked &= markJavaProfilerHook(specs[hook_index].hook); + hooks_marked &= markJavaProfilerHook(specs[hook_index].fork_safe_hook); + } + return hooks_marked; +} + +bool NativeSocketInterposer::isForkChild() { + pid_t current_pid = getpid(); + return current_pid != _hook_owner_pid.load(std::memory_order_acquire); +} + +bool NativeSocketInterposer::setOriginalFunction(int hook_index, void* original) { + switch (hook_index) { + case HOOK_SEND: + _orig_send.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_RECV: + _orig_recv.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_WRITE: + _orig_write.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_READ: + _orig_read.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_CLOSE: + _orig_close.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_DUP2: + _orig_dup2.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_DUP3: + _orig_dup3.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_CONNECT: + _orig_connect.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_ACCEPT: + _orig_accept.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_ACCEPT4: + _orig_accept4.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_RECVFROM: + _orig_recvfrom.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_RECVMSG: + _orig_recvmsg.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_EPOLL_WAIT: + _orig_epoll_wait.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_EPOLL_PWAIT: + _orig_epoll_pwait.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_POLL: + _orig_poll.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_PPOLL: + _orig_ppoll.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_SELECT: + _orig_select.store(reinterpret_cast(original), + std::memory_order_release); + return true; + case HOOK_PSELECT: + _orig_pselect.store(reinterpret_cast(original), + std::memory_order_release); + return true; + default: + return false; + } +} + +bool NativeSocketInterposer::isStreamSocket(int fd) { + return _fd_classifier.isStreamSocket(fd); +} + +bool NativeSocketInterposer::isDatagramSocket(int fd) { + return _fd_classifier.isDatagramSocket(fd); +} + +void NativeSocketInterposer::clearFdType(int fd) { + _fd_classifier.clearFdType(fd); +} + +void NativeSocketInterposer::clearFdTypeCache() { + _fd_classifier.clearFdTypeCache(); +} + +Error NativeSocketInterposer::start() { + clearFdTypeCache(); + if (!markProfilerHooks()) { + Log::warn("NativeSocketInterposer: failed to mark one or more hook symbols; " + "native call stacks for socket samples may contain profiler frames"); + } + _active.store(true, std::memory_order_release); + if (!LibraryPatcher::patch_socket_functions()) { + _active.store(false, std::memory_order_release); + return Error("failed to install native I/O hooks"); + } + return Error::OK; +} + +void NativeSocketInterposer::stop() { + _active.store(false, std::memory_order_release); + LibraryPatcher::unpatch_socket_functions_if_inactive(); + clearFdTypeCache(); +} + +void NativeSocketInterposer::disableAfterPatchFailure() { + _active.store(false, std::memory_order_release); + clearFdTypeCache(); +} + +ssize_t NativeSocketInterposer::send_hook(int fd, const void* buf, size_t len, + int flags) { + if (!NativeSocketInterposer::instance()->active() && NativeSocketSampler::active()) { + return NativeSocketSampler::send_hook(fd, buf, len, flags); + } + return runStreamSocketHook(fd, _orig_send.load(std::memory_order_acquire), 0, + [&](send_fn fn) { return fn(fd, buf, len, flags); }); +} + +ssize_t NativeSocketInterposer::recv_hook(int fd, void* buf, size_t len, + int flags) { + if (!NativeSocketInterposer::instance()->active() && NativeSocketSampler::active()) { + return NativeSocketSampler::recv_hook(fd, buf, len, flags); + } + return runStreamSocketHook(fd, _orig_recv.load(std::memory_order_acquire), 1, + [&](recv_fn fn) { return fn(fd, buf, len, flags); }); +} + +ssize_t NativeSocketInterposer::write_hook(int fd, const void* buf, size_t len) { + if (!NativeSocketInterposer::instance()->active() && NativeSocketSampler::active()) { + return NativeSocketSampler::write_hook(fd, buf, len); + } + return runStreamSocketHook(fd, _orig_write.load(std::memory_order_acquire), 2, + [&](write_fn fn) { return fn(fd, buf, len); }); +} + +ssize_t NativeSocketInterposer::read_hook(int fd, void* buf, size_t len) { + if (!NativeSocketInterposer::instance()->active() && NativeSocketSampler::active()) { + return NativeSocketSampler::read_hook(fd, buf, len); + } + return runStreamSocketHook(fd, _orig_read.load(std::memory_order_acquire), 3, + [&](read_fn fn) { return fn(fd, buf, len); }); +} + +int NativeSocketInterposer::close_hook(int fd) { + int ret; + close_fn original = _orig_close.load(std::memory_order_acquire); + if (original == nullptr) { + ret = static_cast(syscall(SYS_close, fd)); + } else { + ret = original(fd); + } + { + ErrnoGuard errno_guard; + NativeSocketInterposer::instance()->clearFdType(fd); + NativeSocketSampler::instance()->clearFdCacheEntry(fd); + } + return ret; +} + +int NativeSocketInterposer::dup2_hook(int oldfd, int newfd) { + int ret; + dup2_fn original = _orig_dup2.load(std::memory_order_acquire); + if (original == nullptr) { +#ifdef SYS_dup2 + ret = static_cast(syscall(SYS_dup2, oldfd, newfd)); +#else + errno = ENOSYS; + ret = -1; +#endif + } else { + ret = original(oldfd, newfd); + } + { + ErrnoGuard errno_guard; + if (ret >= 0) { + // dup2() implicitly closes newfd before reusing it, so clear stale fd + // classification and address state for the target descriptor. + NativeSocketInterposer::instance()->clearFdType(newfd); + NativeSocketSampler::instance()->clearFdCacheEntry(newfd); + } + } + return ret; +} + +int NativeSocketInterposer::dup3_hook(int oldfd, int newfd, int flags) { + int ret; + dup3_fn original = _orig_dup3.load(std::memory_order_acquire); + if (original == nullptr) { +#ifdef SYS_dup3 + ret = static_cast(syscall(SYS_dup3, oldfd, newfd, flags)); +#else + errno = ENOSYS; + ret = -1; +#endif + } else { + ret = original(oldfd, newfd, flags); + } + { + ErrnoGuard errno_guard; + if (ret >= 0) { + // dup3() implicitly closes newfd before reusing it, so clear stale fd + // classification and address state for the target descriptor. + NativeSocketInterposer::instance()->clearFdType(newfd); + NativeSocketSampler::instance()->clearFdCacheEntry(newfd); + } + } + return ret; +} + +int NativeSocketInterposer::connect_hook(int fd, const struct sockaddr* addr, + socklen_t addrlen) { + bool eligible; + { + ErrnoGuard errno_guard; + eligible = NativeSocketInterposer::instance()->isStreamSocket(fd); + } + return runNativeIoHook(eligible, NativeBlockKind::CONNECT, fd, + _orig_connect.load(std::memory_order_acquire), + [&](connect_fn fn) { return fn(fd, addr, addrlen); }); +} + +int NativeSocketInterposer::accept_hook(int fd, struct sockaddr* addr, + socklen_t* addrlen) { + bool eligible; + { + ErrnoGuard errno_guard; + eligible = NativeSocketInterposer::instance()->isStreamSocket(fd); + } + return runNativeIoHook(eligible, NativeBlockKind::ACCEPT, fd, + _orig_accept.load(std::memory_order_acquire), + [&](accept_fn fn) { return fn(fd, addr, addrlen); }); +} + +int NativeSocketInterposer::accept4_hook(int fd, struct sockaddr* addr, + socklen_t* addrlen, int flags) { + bool eligible; + { + ErrnoGuard errno_guard; + eligible = NativeSocketInterposer::instance()->isStreamSocket(fd); + } + return runNativeIoHook(eligible, NativeBlockKind::ACCEPT, fd, + _orig_accept4.load(std::memory_order_acquire), + [&](accept4_fn fn) { return fn(fd, addr, addrlen, flags); }); +} + +ssize_t NativeSocketInterposer::recvfrom_hook(int fd, void* buf, size_t len, + int flags, struct sockaddr* src_addr, + socklen_t* addrlen) { + return runDatagramSocketHook( + fd, _orig_recvfrom.load(std::memory_order_acquire), [&](recvfrom_fn fn) { + return fn(fd, buf, len, flags, src_addr, addrlen); + }); +} + +ssize_t NativeSocketInterposer::recvmsg_hook(int fd, struct msghdr* msg, int flags) { + return runDatagramSocketHook( + fd, _orig_recvmsg.load(std::memory_order_acquire), [&](recvmsg_fn fn) { + return fn(fd, msg, flags); + }); +} + +int NativeSocketInterposer::epoll_wait_hook(int epfd, struct epoll_event* events, + int maxevents, int timeout) { + bool eligible = maxevents > 0 && timeout != 0; + return runNativeIoHook( + eligible, NativeBlockKind::EPOLL_WAIT, epfd, + _orig_epoll_wait.load(std::memory_order_acquire), + [&](epoll_wait_fn fn) { + return fn(epfd, events, maxevents, timeout); + }); +} + +int NativeSocketInterposer::epoll_pwait_hook(int epfd, struct epoll_event* events, + int maxevents, int timeout, + const sigset_t* sigmask) { + bool eligible = maxevents > 0 && timeout != 0; + return runNativeIoHook( + eligible, NativeBlockKind::EPOLL_WAIT, epfd, + _orig_epoll_pwait.load(std::memory_order_acquire), + [&](epoll_pwait_fn fn) { + return fn(epfd, events, maxevents, timeout, sigmask); + }); +} + +int NativeSocketInterposer::poll_hook(struct pollfd* fds, nfds_t nfds, int timeout) { + bool eligible = fds != nullptr && nfds > 0 && timeout != 0; + return runNativeIoHook(eligible, NativeBlockKind::POLL, 0, + _orig_poll.load(std::memory_order_acquire), + [&](poll_fn fn) { return fn(fds, nfds, timeout); }); +} + +int NativeSocketInterposer::ppoll_hook(struct pollfd* fds, nfds_t nfds, + const struct timespec* timeout_ts, + const sigset_t* sigmask) { + bool eligible = fds != nullptr && nfds > 0 && nonZeroTimespec(timeout_ts); + return runNativeIoHook(eligible, NativeBlockKind::POLL, 0, + _orig_ppoll.load(std::memory_order_acquire), + [&](ppoll_fn fn) { return fn(fds, nfds, timeout_ts, sigmask); }); +} + +int NativeSocketInterposer::select_hook(int nfds, fd_set* readfds, fd_set* writefds, + fd_set* exceptfds, struct timeval* timeout) { + bool eligible = nfds > 0 && nonZeroTimeval(timeout); + return runNativeIoHook(eligible, NativeBlockKind::SELECT, 0, + _orig_select.load(std::memory_order_acquire), + [&](select_fn fn) { + return fn(nfds, readfds, writefds, exceptfds, timeout); + }); +} + +int NativeSocketInterposer::pselect_hook(int nfds, fd_set* readfds, fd_set* writefds, + fd_set* exceptfds, + const struct timespec* timeout_ts, + const sigset_t* sigmask) { + bool eligible = nfds > 0 && nonZeroTimespec(timeout_ts); + return runNativeIoHook(eligible, NativeBlockKind::SELECT, 0, + _orig_pselect.load(std::memory_order_acquire), + [&](pselect_fn fn) { + return fn(nfds, readfds, writefds, exceptfds, + timeout_ts, sigmask); + }); +} + +ssize_t NativeSocketInterposer::fork_safe_send_hook(int fd, const void* buf, + size_t len, int flags) { + if (!isForkChild()) { + return send_hook(fd, buf, len, flags); + } + send_fn original = _orig_send.load(std::memory_order_acquire); + if (original == nullptr) { + errno = ENOSYS; + return -1; + } + return original(fd, buf, len, flags); +} + +ssize_t NativeSocketInterposer::fork_safe_recv_hook(int fd, void* buf, + size_t len, int flags) { + if (!isForkChild()) { + return recv_hook(fd, buf, len, flags); + } + recv_fn original = _orig_recv.load(std::memory_order_acquire); + if (original == nullptr) { + errno = ENOSYS; + return -1; + } + return original(fd, buf, len, flags); +} + +ssize_t NativeSocketInterposer::fork_safe_write_hook(int fd, const void* buf, + size_t len) { + if (!isForkChild()) { + return write_hook(fd, buf, len); + } + write_fn original = _orig_write.load(std::memory_order_acquire); + if (original == nullptr) { + errno = ENOSYS; + return -1; + } + return original(fd, buf, len); +} + +ssize_t NativeSocketInterposer::fork_safe_read_hook(int fd, void* buf, + size_t len) { + if (!isForkChild()) { + return read_hook(fd, buf, len); + } + read_fn original = _orig_read.load(std::memory_order_acquire); + if (original == nullptr) { + errno = ENOSYS; + return -1; + } + return original(fd, buf, len); +} + +int NativeSocketInterposer::fork_safe_close_hook(int fd) { + if (!isForkChild()) { + return close_hook(fd); + } + close_fn original = _orig_close.load(std::memory_order_acquire); + return original == nullptr ? static_cast(syscall(SYS_close, fd)) + : original(fd); +} + +int NativeSocketInterposer::fork_safe_dup2_hook(int oldfd, int newfd) { + if (!isForkChild()) { + return dup2_hook(oldfd, newfd); + } + dup2_fn original = _orig_dup2.load(std::memory_order_acquire); + if (original != nullptr) { + return original(oldfd, newfd); + } +#ifdef SYS_dup2 + return static_cast(syscall(SYS_dup2, oldfd, newfd)); +#else + errno = ENOSYS; + return -1; +#endif +} + +int NativeSocketInterposer::fork_safe_dup3_hook(int oldfd, int newfd, int flags) { + if (!isForkChild()) { + return dup3_hook(oldfd, newfd, flags); + } + dup3_fn original = _orig_dup3.load(std::memory_order_acquire); + if (original != nullptr) { + return original(oldfd, newfd, flags); + } +#ifdef SYS_dup3 + return static_cast(syscall(SYS_dup3, oldfd, newfd, flags)); +#else + errno = ENOSYS; + return -1; +#endif +} + +int NativeSocketInterposer::fork_safe_connect_hook( + int fd, const struct sockaddr* addr, socklen_t addrlen) { + if (!isForkChild()) { + return connect_hook(fd, addr, addrlen); + } + connect_fn original = _orig_connect.load(std::memory_order_acquire); + if (original == nullptr) { + errno = ENOSYS; + return -1; + } + return original(fd, addr, addrlen); +} + +int NativeSocketInterposer::fork_safe_accept_hook(int fd, struct sockaddr* addr, + socklen_t* addrlen) { + if (!isForkChild()) { + return accept_hook(fd, addr, addrlen); + } + accept_fn original = _orig_accept.load(std::memory_order_acquire); + if (original == nullptr) { + errno = ENOSYS; + return -1; + } + return original(fd, addr, addrlen); +} + +int NativeSocketInterposer::fork_safe_accept4_hook(int fd, struct sockaddr* addr, + socklen_t* addrlen, + int flags) { + if (!isForkChild()) { + return accept4_hook(fd, addr, addrlen, flags); + } + accept4_fn original = _orig_accept4.load(std::memory_order_acquire); + if (original == nullptr) { + errno = ENOSYS; + return -1; + } + return original(fd, addr, addrlen, flags); +} + +ssize_t NativeSocketInterposer::fork_safe_recvfrom_hook( + int fd, void* buf, size_t len, int flags, struct sockaddr* src_addr, + socklen_t* addrlen) { + if (!isForkChild()) { + return recvfrom_hook(fd, buf, len, flags, src_addr, addrlen); + } + recvfrom_fn original = _orig_recvfrom.load(std::memory_order_acquire); + if (original == nullptr) { + errno = ENOSYS; + return -1; + } + return original(fd, buf, len, flags, src_addr, addrlen); +} + +ssize_t NativeSocketInterposer::fork_safe_recvmsg_hook(int fd, + struct msghdr* msg, + int flags) { + if (!isForkChild()) { + return recvmsg_hook(fd, msg, flags); + } + recvmsg_fn original = _orig_recvmsg.load(std::memory_order_acquire); + if (original == nullptr) { + errno = ENOSYS; + return -1; + } + return original(fd, msg, flags); +} + +int NativeSocketInterposer::fork_safe_epoll_wait_hook( + int epfd, struct epoll_event* events, int maxevents, int timeout) { + if (!isForkChild()) { + return epoll_wait_hook(epfd, events, maxevents, timeout); + } + epoll_wait_fn original = _orig_epoll_wait.load(std::memory_order_acquire); + if (original == nullptr) { + errno = ENOSYS; + return -1; + } + return original(epfd, events, maxevents, timeout); +} + +int NativeSocketInterposer::fork_safe_epoll_pwait_hook( + int epfd, struct epoll_event* events, int maxevents, int timeout, + const sigset_t* sigmask) { + if (!isForkChild()) { + return epoll_pwait_hook(epfd, events, maxevents, timeout, sigmask); + } + epoll_pwait_fn original = _orig_epoll_pwait.load(std::memory_order_acquire); + if (original == nullptr) { + errno = ENOSYS; + return -1; + } + return original(epfd, events, maxevents, timeout, sigmask); +} + +int NativeSocketInterposer::fork_safe_poll_hook(struct pollfd* fds, + nfds_t nfds, int timeout) { + if (!isForkChild()) { + return poll_hook(fds, nfds, timeout); + } + poll_fn original = _orig_poll.load(std::memory_order_acquire); + if (original == nullptr) { + errno = ENOSYS; + return -1; + } + return original(fds, nfds, timeout); +} + +int NativeSocketInterposer::fork_safe_ppoll_hook( + struct pollfd* fds, nfds_t nfds, const struct timespec* timeout_ts, + const sigset_t* sigmask) { + if (!isForkChild()) { + return ppoll_hook(fds, nfds, timeout_ts, sigmask); + } + ppoll_fn original = _orig_ppoll.load(std::memory_order_acquire); + if (original == nullptr) { + errno = ENOSYS; + return -1; + } + return original(fds, nfds, timeout_ts, sigmask); +} + +int NativeSocketInterposer::fork_safe_select_hook( + int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, + struct timeval* timeout) { + if (!isForkChild()) { + return select_hook(nfds, readfds, writefds, exceptfds, timeout); + } + select_fn original = _orig_select.load(std::memory_order_acquire); + if (original == nullptr) { + errno = ENOSYS; + return -1; + } + return original(nfds, readfds, writefds, exceptfds, timeout); +} + +int NativeSocketInterposer::fork_safe_pselect_hook( + int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, + const struct timespec* timeout_ts, const sigset_t* sigmask) { + if (!isForkChild()) { + return pselect_hook(nfds, readfds, writefds, exceptfds, timeout_ts, sigmask); + } + pselect_fn original = _orig_pselect.load(std::memory_order_acquire); + if (original == nullptr) { + errno = ENOSYS; + return -1; + } + return original(nfds, readfds, writefds, exceptfds, timeout_ts, sigmask); +} + +#else + +NativeSocketInterposer* const NativeSocketInterposer::_instance = new NativeSocketInterposer(); + +#endif diff --git a/ddprof-lib/src/main/cpp/nativeSocketInterposer.h b/ddprof-lib/src/main/cpp/nativeSocketInterposer.h new file mode 100644 index 0000000000..8186ab8fee --- /dev/null +++ b/ddprof-lib/src/main/cpp/nativeSocketInterposer.h @@ -0,0 +1,236 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _NATIVE_SOCKET_INTERPOSER_H +#define _NATIVE_SOCKET_INTERPOSER_H + +#include "arguments.h" + +#include +#include + +#if defined(__linux__) + +#include "codeCache.h" +#include "nativeBlock.h" +#include "nativeFdClassifier.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +class NativeSocketInterposer { +public: + typedef ssize_t (*send_fn)(int, const void*, size_t, int); + typedef ssize_t (*recv_fn)(int, void*, size_t, int); + typedef ssize_t (*write_fn)(int, const void*, size_t); + typedef ssize_t (*read_fn)(int, void*, size_t); + typedef int (*close_fn)(int); + typedef int (*dup2_fn)(int, int); + typedef int (*dup3_fn)(int, int, int); + typedef int (*connect_fn)(int, const struct sockaddr*, socklen_t); + typedef int (*accept_fn)(int, struct sockaddr*, socklen_t*); + typedef int (*accept4_fn)(int, struct sockaddr*, socklen_t*, int); + typedef ssize_t (*recvfrom_fn)(int, void*, size_t, int, struct sockaddr*, socklen_t*); + typedef ssize_t (*recvmsg_fn)(int, struct msghdr*, int); + typedef int (*epoll_wait_fn)(int, struct epoll_event*, int, int); + typedef int (*epoll_pwait_fn)(int, struct epoll_event*, int, int, const sigset_t*); + typedef int (*poll_fn)(struct pollfd*, nfds_t, int); + typedef int (*ppoll_fn)(struct pollfd*, nfds_t, const struct timespec*, const sigset_t*); + typedef int (*select_fn)(int, fd_set*, fd_set*, fd_set*, struct timeval*); + typedef int (*pselect_fn)(int, fd_set*, fd_set*, fd_set*, const struct timespec*, + const sigset_t*); + + enum NativeIoHookIndex : int { + HOOK_SEND = 0, + HOOK_RECV, + HOOK_WRITE, + HOOK_READ, + HOOK_CLOSE, + HOOK_DUP2, + HOOK_DUP3, + HOOK_CONNECT, + HOOK_ACCEPT, + HOOK_ACCEPT4, + HOOK_RECVFROM, + HOOK_RECVMSG, + HOOK_EPOLL_WAIT, + HOOK_EPOLL_PWAIT, + HOOK_POLL, + HOOK_PPOLL, + HOOK_SELECT, + HOOK_PSELECT, + NUM_NATIVE_IO_HOOKS + }; + + struct NativeIoHookSpec { + ImportId import_id; + const char* name; + void* hook; + void* fork_safe_hook; + }; + + static NativeSocketInterposer* instance() { return _instance; } + + Error start(); + void stop(); + void disableAfterPatchFailure(); + bool active() const { return _active.load(std::memory_order_acquire); } + +#ifdef UNIT_TEST + bool setActiveForTest(bool active) { + return _active.exchange(active, std::memory_order_acq_rel); + } + static pid_t setHookOwnerPidForTest(pid_t pid) { + return _hook_owner_pid.exchange(pid, std::memory_order_acq_rel); + } +#endif + + bool isStreamSocket(int fd); + bool isDatagramSocket(int fd); + void clearFdType(int fd); + void clearFdTypeCache(); + + static const NativeIoHookSpec* hookSpecs(); + // Marks every wrapper that can appear in a sampled socket call chain. + // Both sampler-only and TaskBlock startup paths call this because IBM JCL + // bridge imports use the fork-safe interposer layer even when TaskBlock is off. + static bool markProfilerHooks(); + static bool setOriginalFunction(int hook_index, void* original); + static void setHookOwnerPid(pid_t pid) { + _hook_owner_pid.store(pid, std::memory_order_release); + } + + static ssize_t send_hook(int fd, const void* buf, size_t len, int flags); + static ssize_t recv_hook(int fd, void* buf, size_t len, int flags); + static ssize_t write_hook(int fd, const void* buf, size_t len); + static ssize_t read_hook(int fd, void* buf, size_t len); + static int close_hook(int fd); + static int dup2_hook(int oldfd, int newfd); + static int dup3_hook(int oldfd, int newfd, int flags); + static int connect_hook(int fd, const struct sockaddr* addr, socklen_t addrlen); + static int accept_hook(int fd, struct sockaddr* addr, socklen_t* addrlen); + static int accept4_hook(int fd, struct sockaddr* addr, socklen_t* addrlen, int flags); + static ssize_t recvfrom_hook(int fd, void* buf, size_t len, int flags, + struct sockaddr* src_addr, socklen_t* addrlen); + static ssize_t recvmsg_hook(int fd, struct msghdr* msg, int flags); + static int epoll_wait_hook(int epfd, struct epoll_event* events, int maxevents, + int timeout); + static int epoll_pwait_hook(int epfd, struct epoll_event* events, int maxevents, + int timeout, const sigset_t* sigmask); + static int poll_hook(struct pollfd* fds, nfds_t nfds, int timeout); + static int ppoll_hook(struct pollfd* fds, nfds_t nfds, + const struct timespec* timeout_ts, const sigset_t* sigmask); + static int select_hook(int nfds, fd_set* readfds, fd_set* writefds, + fd_set* exceptfds, struct timeval* timeout); + static int pselect_hook(int nfds, fd_set* readfds, fd_set* writefds, + fd_set* exceptfds, const struct timespec* timeout_ts, + const sigset_t* sigmask); + + // These hooks are installed only in JDK libraries that may also execute in + // a post-fork child. In that child they call libc directly, before touching + // profiler TLS, caches, locks, clocks, or recording state. + static ssize_t fork_safe_send_hook(int fd, const void* buf, size_t len, int flags); + static ssize_t fork_safe_recv_hook(int fd, void* buf, size_t len, int flags); + static ssize_t fork_safe_write_hook(int fd, const void* buf, size_t len); + static ssize_t fork_safe_read_hook(int fd, void* buf, size_t len); + static int fork_safe_close_hook(int fd); + static int fork_safe_dup2_hook(int oldfd, int newfd); + static int fork_safe_dup3_hook(int oldfd, int newfd, int flags); + static int fork_safe_connect_hook(int fd, const struct sockaddr* addr, + socklen_t addrlen); + static int fork_safe_accept_hook(int fd, struct sockaddr* addr, + socklen_t* addrlen); + static int fork_safe_accept4_hook(int fd, struct sockaddr* addr, + socklen_t* addrlen, int flags); + static ssize_t fork_safe_recvfrom_hook(int fd, void* buf, size_t len, int flags, + struct sockaddr* src_addr, + socklen_t* addrlen); + static ssize_t fork_safe_recvmsg_hook(int fd, struct msghdr* msg, int flags); + static int fork_safe_epoll_wait_hook(int epfd, struct epoll_event* events, + int maxevents, int timeout); + static int fork_safe_epoll_pwait_hook(int epfd, struct epoll_event* events, + int maxevents, int timeout, + const sigset_t* sigmask); + static int fork_safe_poll_hook(struct pollfd* fds, nfds_t nfds, int timeout); + static int fork_safe_ppoll_hook(struct pollfd* fds, nfds_t nfds, + const struct timespec* timeout_ts, + const sigset_t* sigmask); + static int fork_safe_select_hook(int nfds, fd_set* readfds, fd_set* writefds, + fd_set* exceptfds, struct timeval* timeout); + static int fork_safe_pselect_hook(int nfds, fd_set* readfds, fd_set* writefds, + fd_set* exceptfds, + const struct timespec* timeout_ts, + const sigset_t* sigmask); + + static void setOriginalFunctions(send_fn s, recv_fn r, write_fn w, read_fn rd) { + _orig_send.store(s, std::memory_order_release); + _orig_recv.store(r, std::memory_order_release); + _orig_write.store(w, std::memory_order_release); + _orig_read.store(rd, std::memory_order_release); + } + + static void getOriginalFunctions(send_fn& s, recv_fn& r, write_fn& w, read_fn& rd) { + s = _orig_send.load(std::memory_order_acquire); + r = _orig_recv.load(std::memory_order_acquire); + w = _orig_write.load(std::memory_order_acquire); + rd = _orig_read.load(std::memory_order_acquire); + } + +private: + static NativeSocketInterposer* const _instance; + // Production publishes these once before installing any import hook. Atomic + // access also keeps test overrides and hook reads data-race-free. + static std::atomic _orig_send; + static std::atomic _orig_recv; + static std::atomic _orig_write; + static std::atomic _orig_read; + static std::atomic _orig_close; + static std::atomic _orig_dup2; + static std::atomic _orig_dup3; + static std::atomic _orig_connect; + static std::atomic _orig_accept; + static std::atomic _orig_accept4; + static std::atomic _orig_recvfrom; + static std::atomic _orig_recvmsg; + static std::atomic _orig_epoll_wait; + static std::atomic _orig_epoll_pwait; + static std::atomic _orig_poll; + static std::atomic _orig_ppoll; + static std::atomic _orig_select; + static std::atomic _orig_pselect; + static std::atomic _hook_owner_pid; + + static bool isForkChild(); + + NativeFdClassifier _fd_classifier; + std::atomic _active{false}; + + NativeSocketInterposer() = default; +}; + +#else + +class NativeSocketInterposer { +public: + static NativeSocketInterposer* instance() { return _instance; } + Error start() { return Error::OK; } + void stop() {} + void disableAfterPatchFailure() {} + void clearFdTypeCache() {} + +private: + static NativeSocketInterposer* const _instance; + NativeSocketInterposer() = default; +}; + +#endif + +#endif // _NATIVE_SOCKET_INTERPOSER_H diff --git a/ddprof-lib/src/main/cpp/nativeSocketSampler.cpp b/ddprof-lib/src/main/cpp/nativeSocketSampler.cpp index a8320f69d9..0fc095d034 100644 --- a/ddprof-lib/src/main/cpp/nativeSocketSampler.cpp +++ b/ddprof-lib/src/main/cpp/nativeSocketSampler.cpp @@ -7,13 +7,12 @@ #if defined(__linux__) -#include "codeCache.h" #include "common.h" #include "counters.h" #include "flightRecorder.h" -#include "libraries.h" #include "libraryPatcher.h" #include "log.h" +#include "nativeSocketInterposer.h" #include "os.h" #include "profiler.h" #include "tsc.h" @@ -30,30 +29,6 @@ static thread_local PoissonSampler _send_sampler; static thread_local PoissonSampler _recv_sampler; -// Marks the hook wrapper's own symbol as MARK_JAVA_PROFILER so native call-stack -// unwinding (Profiler::convertNativeTrace) can recognize the boundary between -// profiler-internal frames and the real caller, mirroring MallocHooker::initialize(). -// Resolved by address (not by symbol-name predicate) because these are mangled -// C++ static member functions, unlike malloc_hook's extern "C" free functions. -// Returns false if the symbol could not be resolved/marked, in which case the -// hook boundary is never recognized and every socket sample's native stack -// comes back empty (see NATIVE_TRACE_HOOK_PREFIX_NOT_FOUND). -static bool markJavaProfilerHook(void* fn_addr) { - CodeCache* lib = Libraries::instance()->findLibraryByAddress(fn_addr); - if (lib == nullptr) { - Counters::increment(NATIVE_HOOK_MARK_RESOLVE_FAILED); - return false; - } - const char* name = nullptr; - lib->binarySearch(fn_addr, &name); - if (name == nullptr) { - Counters::increment(NATIVE_HOOK_MARK_RESOLVE_FAILED); - return false; - } - NativeFunc::set_mark(name, MARK_JAVA_PROFILER); - return true; -} - // Debug-only hook-fire counters, paired with TEST_LOG (common.h). Gated at // compile time to keep release hot paths free of cross-thread atomic writes. #ifdef DEBUG @@ -71,6 +46,34 @@ std::atomic NativeSocketSampler::_orig_send{nullp std::atomic NativeSocketSampler::_orig_recv{nullptr}; std::atomic NativeSocketSampler::_orig_write{nullptr}; std::atomic NativeSocketSampler::_orig_read{nullptr}; +std::atomic NativeSocketSampler::_active{false}; + +#ifdef UNIT_TEST +static std::atomic _native_socket_sampler_observer{nullptr}; + +void NativeSocketSampler::setHookObserverForTest(HookObserver observer) { + _native_socket_sampler_observer.store(observer, std::memory_order_release); +} + +uint64_t NativeSocketSampler::socketProbeCountForTest() { + return NativeFdClassifier::probeCountForTest(); +} + +void NativeSocketSampler::resetSocketProbeCountForTest() { + NativeFdClassifier::resetProbeCountForTest(); +} + +void NativeSocketSampler::setProbeOverrideForTest(ProbeOverride probe) { + NativeFdClassifier::setProbeOverrideForTest(probe); +} + +void NativeSocketSampler::observeHookPhaseForTest(const char* phase, int fd, u8 op, ssize_t ret) { + HookObserver observer = _native_socket_sampler_observer.load(std::memory_order_acquire); + if (observer != nullptr) { + observer(phase, fd, op, ret); + } +} +#endif std::string NativeSocketSampler::resolveAddr(int fd) { struct sockaddr_storage ss; @@ -111,51 +114,7 @@ bool NativeSocketSampler::isSocket(int fd) { // Accepts any SOCK_STREAM socket (including AF_UNIX); AF_INET/AF_INET6 filtering // is deferred to resolveAddr() which is only called for sampled events. AF_UNIX // will produce an empty remoteAddress field in the JFR event. - if (fd < 0) return false; - if ((size_t)fd >= (size_t)FD_TYPE_CACHE_SIZE) { - int so_type; - socklen_t solen = sizeof(so_type); - return getsockopt(fd, SOL_SOCKET, SO_TYPE, &so_type, &solen) == 0 - && so_type == SOCK_STREAM; - } - // Acquire on the gen load pairs with the release on the gen-bump in start() - // and on the cache cell store below; without it, on a weakly-ordered arch - // (aarch64) a thread could observe a freshly written cell without the matching - // gen bump (or vice versa), defeating the generation-tag invalidation contract. - uint8_t gen = _fd_cache_gen.load(std::memory_order_acquire); - uint8_t cached = _fd_type_cache[fd].load(std::memory_order_acquire); - // High nibble encodes generation; entry is valid only when it matches current gen mod 16. - if ((cached >> 4) == (gen & 0xF)) { - uint8_t type = cached & 0xF; - // A cached NON_SOCKET verdict is safe to trust: the worst case is that a - // newly-socketed fd reuse under-samples until the next gen reset, which is - // the documented accepted staleness tradeoff. - if (type == FD_TYPE_NON_SOCKET) return false; - // Cached SOCKET: trust the verdict on the hot path; revalidation is deferred - // to recordEvent() on sampled write/read events (see revalidateSocket()). - if (type == FD_TYPE_SOCKET) return true; - } - - int so_type; - socklen_t solen = sizeof(so_type); - int rc = getsockopt(fd, SOL_SOCKET, SO_TYPE, &so_type, &solen); - if (rc == 0) { - bool tcp = (so_type == SOCK_STREAM); - uint8_t type = tcp ? FD_TYPE_SOCKET : FD_TYPE_NON_SOCKET; - _fd_type_cache[fd].store((uint8_t)(((gen & 0xF) << 4) | type), - std::memory_order_release); - return tcp; - } - // Only cache the non-socket verdict when getsockopt definitively says - // "not a socket" (ENOTSOCK). Transient errors (EBADF on a racing close, - // EINTR, etc.) must NOT poison the cache: a sticky misclassification - // would survive fd reuse via dup2() and silently suppress sampling for - // the rest of the session. - if (errno == ENOTSOCK) { - _fd_type_cache[fd].store((uint8_t)(((gen & 0xF) << 4) | FD_TYPE_NON_SOCKET), - std::memory_order_release); - } - return false; + return _fd_classifier.isStreamSocket(fd); } void NativeSocketSampler::insertFdAddrLocked(int fd, std::string addr) { @@ -173,18 +132,24 @@ void NativeSocketSampler::insertFdAddrLocked(int fd, std::string addr) { } } +void NativeSocketSampler::clearFdCacheEntry(int fd) { + _fd_classifier.clearFdType(fd); + + std::lock_guard lock(_fd_cache_mutex); + auto it = _fd_cache.find(fd); + if (it != _fd_cache.end()) { + _fd_lru_list.erase(it->second); + _fd_cache.erase(it); + } +} + bool NativeSocketSampler::revalidateSocket(int fd) { int so_type; socklen_t solen = sizeof(so_type); int rc = getsockopt(fd, SOL_SOCKET, SO_TYPE, &so_type, &solen); if (rc == 0 && so_type == SOCK_STREAM) return true; // fd was reused for a non-socket or is already closed; update the type cache. - if (fd >= 0 && (size_t)fd < (size_t)FD_TYPE_CACHE_SIZE) { - uint8_t gen = _fd_cache_gen.load(std::memory_order_acquire); - _fd_type_cache[fd].store( - (uint8_t)(((gen & 0xF) << 4) | FD_TYPE_NON_SOCKET), - std::memory_order_release); - } + _fd_classifier.cacheNonSocket(fd); return false; } @@ -396,12 +361,9 @@ Error NativeSocketSampler::start(Arguments &args) { // (which carry no latency signal) are suppressed when the interval is large. _rate_limiter.start(init_interval, TARGET_EVENTS_PER_SECOND, PID_WINDOW_SECS, PID_P_GAIN, PID_I_GAIN, PID_D_GAIN, PID_CUTOFF_S); - // Clear the fd->addr cache and reset the fd-type cache generation for the new - // session so stale entries from a prior run cannot produce misattributed events - // even if stop() was not called. clearFdCache() bumps _fd_cache_gen under the - // mutex so the clear and the gen bump are atomic with respect to concurrent - // isSocket() calls. A single call per start() keeps the mod-16 generation-wrap - // budget at the full 16 cycles documented in nativeSocketSampler.h. + // Clear the fd->addr cache and reset the fd-type classifier generation for + // the new session so stale entries from a prior run cannot produce + // misattributed events even if stop() was not called. clearFdCache(); #ifdef DEBUG _send_hook_calls.store(0, std::memory_order_relaxed); @@ -413,19 +375,17 @@ Error NativeSocketSampler::start(Arguments &args) { TEST_LOG("NativeSocketSampler::start interval_ticks=%ld tsc_freq=%llu", init_interval, (unsigned long long)TSC::frequency()); #endif - bool hooks_marked = markJavaProfilerHook((void*)&NativeSocketSampler::send_hook); - hooks_marked &= markJavaProfilerHook((void*)&NativeSocketSampler::recv_hook); - hooks_marked &= markJavaProfilerHook((void*)&NativeSocketSampler::write_hook); - hooks_marked &= markJavaProfilerHook((void*)&NativeSocketSampler::read_hook); - if (!hooks_marked) { + if (!NativeSocketInterposer::markProfilerHooks()) { // Not fatal: hooks are still installed and sampling still works, but - // native stacks for socket samples will come back empty because the - // hook boundary frame can't be recognized during unwinding. + // native stacks may be empty or contain profiler frames because every + // installed wrapper must be recognized during unwinding. Log::warn("NativeSocketSampler: failed to mark one or more hook symbols; " - "native call stacks for socket samples may be empty"); + "native call stacks for socket samples may be incomplete"); } + _active.store(true, std::memory_order_release); if (!LibraryPatcher::patch_socket_functions()) { + _active.store(false, std::memory_order_release); return Error("failed to install native socket hooks (dlsym returned NULL)"); } return Error::OK; @@ -441,18 +401,21 @@ void NativeSocketSampler::stop() { (unsigned long long)_record_accept_calls.load(std::memory_order_relaxed), (unsigned long long)_record_reject_calls.load(std::memory_order_relaxed)); #endif - LibraryPatcher::unpatch_socket_functions(); + _active.store(false, std::memory_order_release); + LibraryPatcher::unpatch_socket_functions_if_inactive(); clearFdCache(); } +void NativeSocketSampler::disableAfterPatchFailure() { + _active.store(false, std::memory_order_release); + _instance->clearFdCache(); +} + void NativeSocketSampler::clearFdCache() { std::lock_guard lock(_fd_cache_mutex); _fd_cache.clear(); _fd_lru_list.clear(); - // Bump the generation under the lock so the clear and the bump are atomic - // with respect to concurrent isSocket() calls: no thread can insert an - // entry tagged with the old generation after the map is cleared. - _fd_cache_gen.fetch_add(1, std::memory_order_release); + _fd_classifier.clearFdTypeCache(); } #else // !__linux__ diff --git a/ddprof-lib/src/main/cpp/nativeSocketSampler.h b/ddprof-lib/src/main/cpp/nativeSocketSampler.h index e45bf60113..d67e709ee4 100644 --- a/ddprof-lib/src/main/cpp/nativeSocketSampler.h +++ b/ddprof-lib/src/main/cpp/nativeSocketSampler.h @@ -13,6 +13,7 @@ #if defined(__linux__) +#include "nativeFdClassifier.h" #include "poissonSampler.h" #include "rateLimiter.h" #include @@ -25,26 +26,22 @@ class LibraryPatcher; // Synchronisation strategy // ------------------------- -// Hook functions (send_hook / recv_hook / write_hook / read_hook) run on the -// calling Java thread, NOT in a signal handler. Therefore malloc and locking -// are safe inside hooks. +// Hook functions (send_hook / recv_hook / write_hook / read_hook) are installed +// in the JDK's libnet and libnio DSOs. IBM's JCL networking bridge in libjava +// uses separate wrappers that bypass all profiler state in a post-fork child. +// Arbitrary JNI libraries remain excluded from code that locks or allocates. // // fd-to-addr cache : guarded by _fd_cache_mutex (std::mutex). // TOCTOU note: the cache is checked under lock, then // released for resolveAddr(); a concurrent thread may // emplace the same fd before re-acquisition. emplace() // is idempotent in that case (first writer wins). -// Address staleness on fd reuse is accepted: worst case -// is one misattributed event per reuse. -// _fd_type_cache : std::atomic array, lock-free. Entry encoding: -// bits [7:4] = generation mod 16, bits [3:0] = type -// (0=unknown, 1=TCP socket, 2=non-TCP). Valid only when -// high nibble matches _fd_cache_gen mod 16. A cached SOCKET -// verdict is trusted on the hot path; revalidation via -// getsockopt() is deferred to recordEvent() for sampled -// write/read events (revalidateSocket()). A cached NON_SOCKET -// verdict is trusted (worst case: a reused fd under-samples -// until the next gen reset). +// Address staleness is possible only after fd reuse +// through unobserved lifecycle paths. +// _fd_classifier : lock-free fd-type classifier shared as code with the +// native I/O interposer. A cached stream-socket verdict +// is trusted on the hot path; sampled write/read events +// revalidate before recording (revalidateSocket()). // _rate_limiter : RateLimiter — owns std::atomic interval, epoch, and // event count. PID update races are resolved by CAS // inside RateLimiter::maybeUpdateInterval(). @@ -71,17 +68,23 @@ class NativeSocketSampler : public Engine { Error check(Arguments &args) override; Error start(Arguments &args) override; void stop() override; + static void disableAfterPatchFailure(); + static bool active() { return _active.load(std::memory_order_acquire); } // Clears the fd-to-address cache and resets the fd-type cache. // Called from both start() (to reset state on restart) and stop(). // Intentionally NOT called on JFR chunk boundaries. void clearFdCache(); + void clearFdCacheEntry(int fd); // PLT hooks installed by LibraryPatcher::patch_socket_functions(). static ssize_t send_hook(int fd, const void* buf, size_t len, int flags); static ssize_t recv_hook(int fd, void* buf, size_t len, int flags); static ssize_t write_hook(int fd, const void* buf, size_t len); static ssize_t read_hook(int fd, void* buf, size_t len); + static ssize_t recordHookResult(int fd, ssize_t ret, u64 t0, u64 t1, u8 op) { + return recordResultForHook(fd, ret, t0, t1, op); + } // Called once by LibraryPatcher::patch_socket_functions() to install the // real libc function pointers before any PLT entries are patched. @@ -100,19 +103,30 @@ class NativeSocketSampler : public Engine { rd = _orig_read.load(std::memory_order_acquire); } +#ifdef UNIT_TEST + static bool setActiveForTest(bool active) { + return _active.exchange(active, std::memory_order_acq_rel); + } + using HookObserver = void (*)(const char* phase, int fd, u8 op, ssize_t ret); + static void setHookObserverForTest(HookObserver observer); + // Compatibility wrappers for sampler tests; probe override/counting is owned + // by NativeFdClassifier now that sampler delegates fd classification to it. + static uint64_t socketProbeCountForTest(); + static void resetSocketProbeCountForTest(); + using ProbeOverride = int (*)(int fd, int *so_type, int *probe_errno); + static void setProbeOverrideForTest(ProbeOverride probe); +#endif + private: static NativeSocketSampler* const _instance; - // Set by setOriginalFunctions() (called under _lock, before PLT patching) and - // read by the hooks on arbitrary application threads. Declared std::atomic with - // release/acquire pairing so a stop()→start() restart cycle, which rewrites these - // pointers while a stale-epoch hook may still be in flight, has no data race and no - // value tearing on any memory model. The acquire load in each hook also pairs with - // the release store here to publish the pointer before the hook observes it. + // Production publishes these once before PLT patching. Atomic access pairs + // that publication with hook reads and also keeps test overrides data-race-free. static std::atomic _orig_send; static std::atomic _orig_recv; static std::atomic _orig_write; static std::atomic _orig_read; + static std::atomic _active; // Target aggregate event rate: ~83 events/s (~5000/min) across all four hooks // (send/write and recv/read) combined. @@ -147,30 +161,7 @@ class NativeSocketSampler : public Engine { std::unordered_map _fd_cache; std::mutex _fd_cache_mutex; - // fd-type cache for write/read hooks. Lock-free: one atomic byte per fd number. - // Encoding: bits [7:4] = generation mod 16, bits [3:0] = type (0=unknown/invalid - // — implicit zero in fresh array, never written explicitly; 1=TCP socket; - // 2=non-TCP). An entry is valid only when its high nibble equals _fd_cache_gen - // mod 16. Incrementing _fd_cache_gen invalidates all entries in O(1) without - // touching the 65536-entry array. - // - // KNOWN LIMITATION (mod-16 generation wrap): _fd_cache_gen is only consulted via - // its low 4 bits. After 16 start() cycles the generation wraps and stale entries - // from a previous incarnation become indistinguishable from current ones until each - // fd is naturally re-probed. Profiler restarts are not exercised in production - // (only in tests), so the wrap is benign in practice. If restart-in-prod ever - // becomes a supported mode, widen _fd_cache_gen to uint32_t and store the full - // generation in a wider per-fd cell. - // Fds outside [0, FD_TYPE_CACHE_SIZE) are probed on every call. - static const int FD_TYPE_CACHE_SIZE = 65536; - // FD_TYPE_UNKNOWN is the implicit value-zero sentinel for never-written entries - // and gen-mismatch entries; it is decoded by the (cached >> 4) != gen path in - // isSocket(), not by an explicit comparison against this constant. - static const uint8_t FD_TYPE_UNKNOWN = 0; - static const uint8_t FD_TYPE_SOCKET = 1; - static const uint8_t FD_TYPE_NON_SOCKET = 2; - std::atomic _fd_cache_gen{0}; // incremented on each cache reset - std::atomic _fd_type_cache[FD_TYPE_CACHE_SIZE]; + NativeFdClassifier _fd_classifier; NativeSocketSampler() = default; @@ -193,17 +184,28 @@ class NativeSocketSampler : public Engine { std::lock_guard lock(_fd_cache_mutex); return (int)_fd_cache.size(); } + bool fdAddrCacheContainsForTest(int fd) { + std::lock_guard lock(_fd_cache_mutex); + return _fd_cache.find(fd) != _fd_cache.end(); + } void fdAddrCacheInsertForTest(int fd, const std::string& addr) { std::lock_guard lock(_fd_cache_mutex); insertFdAddrLocked(fd, addr); } + bool isSocketForTest(int fd) { + return isSocket(fd); + } +#ifdef UNIT_TEST + bool revalidateSocketForTest(int fd) { + return revalidateSocket(fd); + } +#endif private: // Returns true if fd is a SOCK_STREAM socket (including AF_UNIX). - // Uses the fd-type cache; calls getsockopt on first encounter per fd and on - // every cached-SOCKET hit to revalidate against fd reuse (a closed socket fd - // reassigned to a regular file/pipe must not keep emitting socket events). + // Uses the fd classifier; calls getsockopt on first encounter per fd. + // Cached SOCKET verdicts are revalidated only on sampled write/read events. bool isSocket(int fd); // Decide whether to sample and compute weight. @@ -223,6 +225,17 @@ class NativeSocketSampler : public Engine { if (ret > 0) _instance->recordEvent(fd, t0, t1, ret, op); return ret; } + + static inline ssize_t recordResultForHook(int fd, ssize_t ret, u64 t0, u64 t1, u8 op) { +#ifdef UNIT_TEST + observeHookPhaseForTest("record", fd, op, ret); +#endif + return record_if_positive(fd, ret, t0, t1, op); + } + +#ifdef UNIT_TEST + static void observeHookPhaseForTest(const char* phase, int fd, u8 op, ssize_t ret); +#endif }; #else // !__linux__ @@ -233,7 +246,9 @@ class NativeSocketSampler : public Engine { Error check(Arguments &args) override { return Error::OK; } Error start(Arguments &args) override { return Error::OK; } void stop() override {} + static void disableAfterPatchFailure() {} void clearFdCache() {} + void clearFdCacheEntry(int fd) { (void)fd; } private: static NativeSocketSampler* const _instance; NativeSocketSampler() {} diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 29b364cf1b..ff456dc14c 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -8,6 +8,7 @@ #include "profiler.h" #include "asyncSampleMutex.h" #include "mallocTracer.h" +#include "nativeSocketInterposer.h" #include "nativeSocketSampler.h" #include "context.h" #include "guards.h" @@ -1755,6 +1756,12 @@ Error Profiler::start(Arguments &args, bool reset) { setTaskBlockEnabled( (activated & EM_WALL) && args._wall_precheck && track_unfiltered_wall); + if (taskBlockEnabled()) { + Error native_io_error = NativeSocketInterposer::instance()->start(); + if (native_io_error) { + Log::warn("%s", native_io_error.message()); + } + } _state.store(RUNNING, std::memory_order_release); _start_time = time(NULL); __atomic_add_fetch(&_epoch, 1, __ATOMIC_RELAXED); @@ -1812,6 +1819,7 @@ Error Profiler::stop() { // it can see _socket_active=true, wait for the lock, then re-patch PLT slots // that unpatch just restored. Stopping the refresher here closes that window. _libs->stopRefresher(); + NativeSocketInterposer::instance()->stop(); if (_event_mask & EM_NATIVESOCKET) NativeSocketSampler::instance()->stop(); if (_event_mask & EM_WALL) diff --git a/ddprof-lib/src/main/cpp/profiler.h b/ddprof-lib/src/main/cpp/profiler.h index 2c20c1efb7..1a100e8f48 100644 --- a/ddprof-lib/src/main/cpp/profiler.h +++ b/ddprof-lib/src/main/cpp/profiler.h @@ -467,6 +467,9 @@ class alignas(alignof(SpinLock)) Profiler { int tid, jthread thread, int start_depth, TaskBlockEvent *event); static void setTaskBlockRecordOverrideForTest( TaskBlockRecordOverride override); + bool setTaskBlockEnabledForTest(bool enabled) { + return _task_block_enabled.exchange(enabled, std::memory_order_acq_rel); + } #endif bool tryEnterTaskBlockActivity(); void leaveTaskBlockActivity(); @@ -501,7 +504,7 @@ class alignas(alignof(SpinLock)) Profiler { bool taskBlockRotationActiveForTest() const { return _task_block_rotation.load(std::memory_order_acquire); } - int taskBlockInflightForTest() const { + u64 taskBlockInflightForTest() const { return _task_block_inflight.load(std::memory_order_acquire); } diff --git a/ddprof-lib/src/main/cpp/symbols.h b/ddprof-lib/src/main/cpp/symbols.h index b315d51ef5..81a4fb82e1 100644 --- a/ddprof-lib/src/main/cpp/symbols.h +++ b/ddprof-lib/src/main/cpp/symbols.h @@ -1,5 +1,6 @@ /* * Copyright The async-profiler authors + * Copyright 2026, Datadog, Inc. * SPDX-License-Identifier: Apache-2.0 */ @@ -41,9 +42,13 @@ class UnloadProtection { UnloadProtection(const CodeCache *cc); ~UnloadProtection(); + UnloadProtection(const UnloadProtection& other) = delete; UnloadProtection& operator=(const UnloadProtection& other) = delete; + UnloadProtection(UnloadProtection&& other) noexcept; + UnloadProtection& operator=(UnloadProtection&& other) noexcept; bool isValid() const { return _valid; } + void* release(); }; #endif // _SYMBOLS_H diff --git a/ddprof-lib/src/main/cpp/symbols_linux.cpp b/ddprof-lib/src/main/cpp/symbols_linux.cpp index b328fcfd56..9da8470c33 100644 --- a/ddprof-lib/src/main/cpp/symbols_linux.cpp +++ b/ddprof-lib/src/main/cpp/symbols_linux.cpp @@ -1,5 +1,6 @@ /* * Copyright The async-profiler authors + * Copyright 2026, Datadog, Inc. * SPDX-License-Identifier: Apache-2.0 */ @@ -348,7 +349,7 @@ class ElfParser { bool _relocate_dyn; ElfHeader* _header; const char* _sections; - const char* _vaddr_diff; + uintptr_t _load_bias; const char* _image_end; // one-past-the-end of the mapped ELF image; bounds file-relative reads ElfParser(CodeCache* cc, const char* base, const void* addr, size_t image_size, const char* file_name, bool relocate_dyn) { @@ -357,6 +358,7 @@ class ElfParser { _file_name = file_name; _relocate_dyn = relocate_dyn; _header = (ElfHeader*)addr; + _load_bias = 0; _image_end = (const char*)addr + image_size; // e_shoff sits at a fixed offset inside the header; only compute the pointer // when the image is at least header-sized AND e_shoff is within the image, @@ -450,25 +452,34 @@ class ElfParser { return inImage(ph, sizeof(ElfProgramHeader)) ? ph : NULL; } - const char* at(ElfProgramHeader* pheader) { - if (_header->e_type == ET_EXEC) { - return (const char*)pheader->p_vaddr; + const char* addressAt(uint64_t virtual_address, size_t extra_offset = 0) const { + if (virtual_address > UINTPTR_MAX) { + return NULL; + } + uintptr_t offset = (uintptr_t)virtual_address; + if (extra_offset > UINTPTR_MAX - offset) { + return NULL; + } + offset += extra_offset; + + uintptr_t load_bias = _header->e_type == ET_EXEC ? 0 : _load_bias; + if (load_bias > UINTPTR_MAX - offset) { + return NULL; } - return _vaddr_diff == NULL ? (const char*)pheader->p_vaddr : _vaddr_diff + pheader->p_vaddr; + return (const char*)(load_bias + offset); } - const char* base() { - return _header->e_type == ET_EXEC ? NULL : _vaddr_diff; + const char* at(ElfProgramHeader* pheader) { + return addressAt(pheader->p_vaddr); } char* dyn_ptr(ElfDyn* dyn) { // GNU dynamic linker relocates pointers in the dynamic section, while musl doesn't. // Also, [vdso] is not relocated, and its vaddr may differ from the load address. - if (_relocate_dyn || (_base != NULL && (char*)dyn->d_un.d_ptr < _base)) { - return _vaddr_diff == NULL ? (char*)dyn->d_un.d_ptr : (char*)_vaddr_diff + dyn->d_un.d_ptr; - } else { - return (char*)dyn->d_un.d_ptr; + if (_relocate_dyn || (_base != NULL && dyn->d_un.d_ptr < (uintptr_t)_base)) { + return (char*)addressAt(dyn->d_un.d_ptr); } + return dyn->d_un.d_ptr <= UINTPTR_MAX ? (char*)(uintptr_t)dyn->d_un.d_ptr : NULL; } ElfSection* findSection(uint32_t type, const char* name); @@ -569,17 +580,18 @@ void ElfParser::parseProgramHeaders(CodeCache* cc, const char* base, const char* void ElfParser::calcVirtualLoadAddress() { // Find a difference between the virtual load address (often zero) and the actual DSO base if (_base == NULL) { - _vaddr_diff = NULL; + _load_bias = 0; return; } for (int i = 0; i < _header->e_phnum; i++) { ElfProgramHeader* pheader = phdrAt(i); if (pheader != NULL && pheader->p_type == PT_LOAD) { - _vaddr_diff = _base - pheader->p_vaddr; + // p_vaddr is an ELF integer address, not an offset into the C++ object at _base. + _load_bias = (uintptr_t)_base - (uintptr_t)pheader->p_vaddr; return; } } - _vaddr_diff = _base; + _load_bias = (uintptr_t)_base; } void ElfParser::parseDynamicSection() { @@ -665,7 +677,6 @@ void ElfParser::parseDynamicSection() { loadSymbolTable(symtab, syment * nsyms, syment, strtab, strsz); } - const char* base = this->base(); if (jmprel != NULL && pltrelsz != 0) { // Parse .rela.plt table for (size_t offs = 0; offs < pltrelsz; offs += relent) { @@ -674,7 +685,10 @@ void ElfParser::parseDynamicSection() { if (sym->st_name != 0) { const char* sym_name = strAt(strtab, strsz, sym->st_name); if (sym_name != NULL) { - _cc->addImport((void**)(base + r->r_offset), sym_name); + const char* location = addressAt(r->r_offset); + if (location != NULL) { + _cc->addImport((void**)location, sym_name); + } } } } @@ -691,7 +705,10 @@ void ElfParser::parseDynamicSection() { if (sym->st_name != 0) { const char* sym_name = strAt(strtab, strsz, sym->st_name); if (sym_name != NULL) { - _cc->addImport((void**)(base + r->r_offset), sym_name); + const char* location = addressAt(r->r_offset); + if (location != NULL) { + _cc->addImport((void**)location, sym_name); + } } } } @@ -792,7 +809,10 @@ void ElfParser::loadSymbols(bool use_debug) { _cc->setPlt(plt->sh_addr, plt->sh_size); ElfSection* reltab = findSection(SHT_RELA, ".rela.plt"); if (reltab != NULL || (reltab = findSection(SHT_REL, ".rel.plt")) != NULL) { - addRelocationSymbols(reltab, base() + plt->sh_addr + PLT_HEADER_SIZE); + const char* plt_address = addressAt(plt->sh_addr, PLT_HEADER_SIZE); + if (plt_address != NULL) { + addRelocationSymbols(reltab, plt_address); + } } } } @@ -942,45 +962,31 @@ void ElfParser::loadSymbolTable(const char* symbols, size_t total_size, size_t e if (ent_size < sizeof(ElfSymbol)) { return; } - const char* base = this->base(); // Iterate by a size_t offset rather than incrementing the pointer: a huge // attacker-controlled ent_size would otherwise overflow `symbols + ent_size` // to a small pointer that still compares <= end, walking off the image. The // `ent_size <= total_size - off` form keeps off <= total_size with no overflow. for (size_t off = 0; ent_size <= total_size - off; off += ent_size) { - ElfSymbol* sym = (ElfSymbol*)(symbols + off); - if (sym->st_name != 0 && sym->st_value != 0) { + // Section contents are byte-addressed and a malformed sh_offset need not + // satisfy ElfSymbol alignment. Copying also keeps every field read within + // the sizeof(ElfSymbol) range validated by the loop condition. + ElfSymbol sym; + memcpy(&sym, symbols + off, sizeof(sym)); + if (sym.st_name != 0 && sym.st_value != 0) { // Resolve the name through the bounded string table; a bad st_name // offset (or unterminated string) drops the symbol instead of reading // out of bounds. - const char* sym_name = strAt(strings, strings_size, sym->st_name); + const char* sym_name = strAt(strings, strings_size, sym.st_name); if (sym_name == NULL) { continue; } // Skip special AArch64 mapping symbols: $x and $d - if (sym->st_size != 0 || sym->st_info != 0 || sym_name[0] != '$') { - const char* addr; - if (base != NULL) { - // Check for overflow when adding sym->st_value to base - uintptr_t base_addr = (uintptr_t)base; - uint64_t symbol_value = sym->st_value; - - // Skip this symbol if addition would overflow - // First check if symbol_value exceeds the address space - if (symbol_value > UINTPTR_MAX) { - continue; - } - // Then check if addition would overflow - if (base_addr > UINTPTR_MAX - (uintptr_t)symbol_value) { - continue; - } - - // Perform addition using integer arithmetic to avoid pointer overflow - addr = (const char*)(base_addr + (uintptr_t)symbol_value); - } else { - addr = (const char*)sym->st_value; + if (sym.st_size != 0 || sym.st_info != 0 || sym_name[0] != '$') { + const char* addr = addressAt(sym.st_value); + if (addr == NULL) { + continue; } - _cc->add(addr, (int)sym->st_size, sym_name); + _cc->add(addr, (int)sym.st_size, sym_name); } } } @@ -1183,7 +1189,12 @@ void Symbols::parseLibraries(CodeCacheArray* array, bool kernel_symbols) { if (strchr(lib.file, ':') != NULL) { // Do not try to parse pseudofiles like anon_inode:name, /memfd:name } else if (strcmp(lib.file, "[vdso]") == 0) { + // A sanitizer build can place the VDSO outside its instrumented + // application range. Reading that mapping then faults in the + // compiler-generated shadow-memory check before the ELF parser runs. +#if !defined(ASAN_ENABLED) && !defined(TSAN_ENABLED) ElfParser::parseProgramHeaders(cc, lib.map_start, lib.map_end, true); +#endif } else if (lib.image_base == NULL) { // Unlikely case when image base has not been found: not safe to access program headers. // Be careful: executable file is not always ELF, e.g. classes.jsa @@ -1258,6 +1269,32 @@ UnloadProtection::~UnloadProtection() { } } +UnloadProtection::UnloadProtection(UnloadProtection&& other) noexcept + : _lib_handle(other._lib_handle), _valid(other._valid) { + other._lib_handle = NULL; + other._valid = false; +} + +UnloadProtection& UnloadProtection::operator=(UnloadProtection&& other) noexcept { + if (this != &other) { + if (_lib_handle != NULL) { + dlclose(_lib_handle); + } + _lib_handle = other._lib_handle; + _valid = other._valid; + other._lib_handle = NULL; + other._valid = false; + } + return *this; +} + +void* UnloadProtection::release() { + void* handle = _lib_handle; + _lib_handle = NULL; + _valid = false; + return handle; +} + void Symbols::initLibraryRanges() { init_lib_ranges_once(); } diff --git a/ddprof-lib/src/main/cpp/symbols_macos.cpp b/ddprof-lib/src/main/cpp/symbols_macos.cpp index e09c0f1490..55353ccc20 100644 --- a/ddprof-lib/src/main/cpp/symbols_macos.cpp +++ b/ddprof-lib/src/main/cpp/symbols_macos.cpp @@ -29,6 +29,32 @@ UnloadProtection::~UnloadProtection() { } } +UnloadProtection::UnloadProtection(UnloadProtection&& other) noexcept + : _lib_handle(other._lib_handle), _valid(other._valid) { + other._lib_handle = NULL; + other._valid = false; +} + +UnloadProtection& UnloadProtection::operator=(UnloadProtection&& other) noexcept { + if (this != &other) { + if (_lib_handle != NULL) { + dlclose(_lib_handle); + } + _lib_handle = other._lib_handle; + _valid = other._valid; + other._lib_handle = NULL; + other._valid = false; + } + return *this; +} + +void* UnloadProtection::release() { + void* handle = _lib_handle; + _lib_handle = NULL; + _valid = false; + return handle; +} + class MachOParser { private: CodeCache* _cc; diff --git a/ddprof-lib/src/main/cpp/threadFilter.cpp b/ddprof-lib/src/main/cpp/threadFilter.cpp index ad9e31ea54..6459795a25 100644 --- a/ddprof-lib/src/main/cpp/threadFilter.cpp +++ b/ddprof-lib/src/main/cpp/threadFilter.cpp @@ -701,7 +701,8 @@ bool ThreadFilter::ownedBlockGeneration(const ThreadEntry& entry, bool suppressible_state = state == OSThreadState::SLEEPING || state == OSThreadState::CONDVAR_WAIT || state == OSThreadState::OBJECT_WAIT || - state == OSThreadState::MONITOR_WAIT; + state == OSThreadState::MONITOR_WAIT || + state == OSThreadState::IO_WAIT; if (!suppressible_state) return false; RecordingEpoch epoch = recordingEpoch(); @@ -713,8 +714,14 @@ bool ThreadFilter::ownedBlockGeneration(const ThreadEntry& entry, u64 block_generation = slot->blockGeneration(); BlockRunOwner owner = slot->activeBlockOwner(); + // Native hooks own TaskBlock recording at completion, + // so waiting for a MethodSample would let the first wall signal interrupt + // the syscall and end this generation before suppression can take effect. + // Java and JVMTI owners retain their first successful MethodSample. + bool sample_required = + require_sampled && owner != BlockRunOwner::NATIVE; if (owner == BlockRunOwner::NONE || - (require_sampled && + (sample_required && slot->sampledBlockGeneration() != block_generation)) { return false; } @@ -731,7 +738,7 @@ bool ThreadFilter::ownedBlockGeneration(const ThreadEntry& entry, slot->blockGeneration() != block_generation || slot->activeBlockState() != state || slot->nativeTid() != entry.tid || slot->lifecycleGeneration() != entry.lifecycle_generation || - (require_sampled && + (sample_required && slot->sampledBlockGeneration() != block_generation)) { return false; } diff --git a/ddprof-lib/src/main/cpp/threadLocalData.h b/ddprof-lib/src/main/cpp/threadLocalData.h index 851f89a4c5..4587ac5eb5 100644 --- a/ddprof-lib/src/main/cpp/threadLocalData.h +++ b/ddprof-lib/src/main/cpp/threadLocalData.h @@ -8,6 +8,7 @@ #include "context.h" #include "nativeMem.h" +#include "context_api.h" #include "otel_context.h" #include "os.h" #include "threadLocal.h" @@ -361,6 +362,25 @@ class ProfiledThread : public ThreadLocalData { _otel_local_root_span_id = 0; } +#ifdef UNIT_TEST + void setContextForTest(u64 span_id, u64 root_span_id) { + ContextApi::initializeContextTLS(this); + for (int i = 7; i >= 0; i--) { + _otel_ctx_record.span_id[i] = static_cast(span_id & 0xff); + span_id >>= 8; + } + _otel_local_root_span_id = root_span_id; + __atomic_store_n(&_otel_ctx_record.valid, 1, __ATOMIC_RELEASE); + } + + void clearContextForTest() { + if (_otel_ctx_initialized) { + __atomic_store_n(&_otel_ctx_record.valid, 0, __ATOMIC_RELEASE); + } + clearOtelSidecar(); + } +#endif + inline bool parkEnter(u64 start_ticks, const Context& context) { u32 flags = __atomic_load_n(&_misc_flags, __ATOMIC_ACQUIRE); while ((flags & FLAG_PARKED) == 0) { diff --git a/ddprof-lib/src/main/cpp/threadState.h b/ddprof-lib/src/main/cpp/threadState.h index 786c96fb6f..0fc0141dc0 100644 --- a/ddprof-lib/src/main/cpp/threadState.h +++ b/ddprof-lib/src/main/cpp/threadState.h @@ -16,8 +16,9 @@ enum class OSThreadState : int { BREAKPOINTED = 6, // Suspended at breakpoint SLEEPING = 7, // Thread.sleep() TERMINATED = 8, // All done, but not reclaimed yet - SYSCALL = 9 // does not originate in the JVM, used when the current frame is - // known to be a syscall + SYSCALL = 9, // does not originate in the JVM, used when the current frame is + // known to be a syscall + IO_WAIT = 10 // Physical platform/carrier thread blocked in native I/O }; enum class ExecutionMode : int { diff --git a/ddprof-lib/src/main/cpp/wallClock.cpp b/ddprof-lib/src/main/cpp/wallClock.cpp index d722ee1b0b..6b0e9808ae 100644 --- a/ddprof-lib/src/main/cpp/wallClock.cpp +++ b/ddprof-lib/src/main/cpp/wallClock.cpp @@ -33,7 +33,8 @@ static inline bool isPrecheckSuppressionState(OSThreadState state) { return state == OSThreadState::SLEEPING || state == OSThreadState::CONDVAR_WAIT || state == OSThreadState::OBJECT_WAIT || - state == OSThreadState::MONITOR_WAIT; + state == OSThreadState::MONITOR_WAIT || + state == OSThreadState::IO_WAIT; } static inline u64 loadSpanId(OtelThreadContextRecord* record) { @@ -71,6 +72,11 @@ struct WallPrecheckResult { OSThreadState flush_state = OSThreadState::UNKNOWN; }; +enum class UnownedBlockedFallback { + DISABLED, + ENABLED, +}; + static inline void incrementSuppressedOwnedBlock() { Counters::increment(WC_SIGNAL_SUPPRESSED_OWNED_BLOCK); WallClockCounters::incrementSuppressedOwnedBlock(); @@ -85,7 +91,8 @@ static inline bool suppressOwnedBlock(const ThreadEntry& entry) { } static inline WallPrecheckResult prepareWallPrecheck(ProfiledThread* current, - bool precheck) { + bool precheck, + UnownedBlockedFallback fallback) { WallPrecheckResult result; if (current == nullptr || !precheck || hasKnownActiveTraceContext(current)) { return result; @@ -103,9 +110,9 @@ static inline WallPrecheckResult prepareWallPrecheck(ProfiledThread* current, return result; } - // Owned blocks replace repeated signals only after their current generation - // has produced one MethodSample. Context-scoped profiling must continue - // sampling its selected threads normally. + // Native-owned blocks suppress immediately because their completion hook + // records an eligible TaskBlock stack. Other owners retain their first + // successful MethodSample. Context-scoped profiling continues sampling normally. if (!registry->unfilteredWallTrackingActive() || slot->inContextWindow()) { return result; } @@ -129,6 +136,13 @@ static inline WallPrecheckResult prepareWallPrecheck(ProfiledThread* current, return result; } + // Suppressed tails require a recorded call trace for deferred replay. The + // delegated JVMTI path does not return one, so it keeps unowned observations + // on ordinary per-signal sampling. + if (fallback == UnownedBlockedFallback::DISABLED) { + return result; + } + result.observed_state = getOSThreadState(); result.observed_state_valid = true; if (isPrecheckSuppressionState(result.observed_state)) { @@ -256,7 +270,8 @@ void WallClockASGCT::signalHandler(int signo, siginfo_t *siginfo, void *ucontext // its first successful MethodSample and suppresses subsequent signals. // Unowned blocked observations use weighted fallback sampling because raw OS // state cannot distinguish one long sleep from several shorter runs. - WallPrecheckResult precheck = prepareWallPrecheck(current, _precheck); + WallPrecheckResult precheck = prepareWallPrecheck( + current, _precheck, UnownedBlockedFallback::ENABLED); if (precheck.suppress) { return; } @@ -391,8 +406,8 @@ void WallClockASGCT::timerLoop() { registry_lookups++; thread_filter->lookupThreadEntry(entry, recording_epoch); } - // Timer-thread fast path (wallprecheck=true): skip the kernel IPI only - // after an explicitly owned run has recorded its first MethodSample. + // Timer-thread fast path (wallprecheck=true): native hooks suppress + // immediately; other owners suppress after their first MethodSample. if (_precheck && suppressOwnedBlock(entry)) { return WallClockCandidateOutcome::PRECHECK_REJECTED; } @@ -469,7 +484,8 @@ void WallClockJvmti::signalHandler(int signo, siginfo_t *siginfo, errno = saved_errno; return; } - WallPrecheckResult precheck = prepareWallPrecheck(current, _precheck); + WallPrecheckResult precheck = prepareWallPrecheck( + current, _precheck, UnownedBlockedFallback::DISABLED); if (precheck.suppress) { errno = saved_errno; return; diff --git a/ddprof-lib/src/test/cpp/elfparser_ut.cpp b/ddprof-lib/src/test/cpp/elfparser_ut.cpp index c0e7cf5842..4753ffe1e3 100644 --- a/ddprof-lib/src/test/cpp/elfparser_ut.cpp +++ b/ddprof-lib/src/test/cpp/elfparser_ut.cpp @@ -1,9 +1,15 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + #ifdef __linux__ #include #include #include "codeCache.h" +#include "common.h" #include "libraries.h" #include "symbols.h" #include "symbols_linux.h" @@ -118,6 +124,22 @@ TEST_F(ElfReladyn, resolveFromRela_dyn_R_ABS64) { ASSERT_THAT(sym, ::testing::NotNull()); } +TEST_F(ElfReladyn, resolvesEveryLocationForTheSameImport) { + ASSERT_EQ(3u, libreladyn()->importCount(im_read)); + + void** first = libreladyn()->findImport(im_read, 0); + void** second = libreladyn()->findImport(im_read, 1); + void** third = libreladyn()->findImport(im_read, 2); + + ASSERT_THAT(first, ::testing::NotNull()); + ASSERT_THAT(second, ::testing::NotNull()); + ASSERT_THAT(third, ::testing::NotNull()); + EXPECT_NE(first, second); + EXPECT_NE(first, third); + EXPECT_NE(second, third); + EXPECT_THAT(libreladyn()->findImport(im_read, 3), ::testing::IsNull()); +} + class ElfTest : public ::testing::Test { protected: void SetUp() override { @@ -319,6 +341,12 @@ INSTANTIATE_TEST_SUITE_P( #else TEST_P(ElfTestParam, invalidElfSmallMappingAfterUnmap) { +#if defined(TSAN_ENABLED) + // This stress test deliberately overlaps dlclose's writes to the DSO mapping + // with ELF parser reads. That intentional test mechanism is a data race, so + // TSan must report it even when the mapping remains valid for the read. + GTEST_SKIP() << "concurrent dlclose stress is incompatible with TSan"; +#endif char cwd[PATH_MAX - 64]; if (getcwd(cwd, sizeof(cwd)) == nullptr) { exit(1); diff --git a/ddprof-lib/src/test/cpp/nativeBlock_ut.cpp b/ddprof-lib/src/test/cpp/nativeBlock_ut.cpp new file mode 100644 index 0000000000..f101e43e60 --- /dev/null +++ b/ddprof-lib/src/test/cpp/nativeBlock_ut.cpp @@ -0,0 +1,370 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#if defined(__linux__) + +#include "context_api.h" +#include "counters.h" +#include "nativeBlock.h" +#include "profiler.h" +#include "threadLocalData.h" +#include "tsc.h" + +#include +#include +#include +#include +#include + +namespace { + +std::atomic g_record_calls{0}; +int g_record_tid = -1; +jthread g_record_thread = reinterpret_cast(1); +int g_record_start_depth = -1; +TaskBlockEvent g_record_event{}; + +Profiler::TaskBlockRecordResult recordTaskBlockSuccessForTest( + int tid, jthread thread, int start_depth, TaskBlockEvent* event) { + g_record_tid = tid; + g_record_thread = thread; + g_record_start_depth = start_depth; + g_record_event = *event; + g_record_calls.fetch_add(1, std::memory_order_relaxed); + return Profiler::TaskBlockRecordResult::RECORDED; +} + +class ScopedTaskBlockEnabled { +public: + explicit ScopedTaskBlockEnabled(bool enabled) + : _saved(Profiler::instance()->setTaskBlockEnabledForTest(enabled)) {} + ~ScopedTaskBlockEnabled() { + Profiler::instance()->setTaskBlockEnabledForTest(_saved); + } + +private: + bool _saved; +}; + +class CurrentThreadScope { +public: + CurrentThreadScope() { + ProfiledThread::initCurrentThread(); + _thread = ProfiledThread::current(); + _thread->clearContextForTest(); + _thread->setFilterSlotId(-1); + _thread->setJavaThread(false); + } + ~CurrentThreadScope() { + if (_thread != nullptr) { + _thread->clearContextForTest(); + } + ProfiledThread::release(); + } + + ProfiledThread* thread() const { return _thread; } + + void releaseOwnership() { _thread = nullptr; } + +private: + ProfiledThread* _thread; +}; + +class DetachedCurrentThread { +public: + explicit DetachedCurrentThread(CurrentThreadScope& current) + : _thread(ProfiledThread::clearCurrentThreadTLS()) { + current.releaseOwnership(); + } + ~DetachedCurrentThread() { + if (_thread != nullptr) { + ProfiledThread::deleteForTest(_thread); + } + } + +private: + ProfiledThread* _thread; +}; + +class NativeBlockScopeTest : public ::testing::Test { +protected: + void SetUp() override { + Counters::reset(); + Profiler::setTaskBlockRecordOverrideForTest(recordTaskBlockSuccessForTest); + g_record_calls = 0; + g_record_tid = -1; + g_record_thread = reinterpret_cast(1); + g_record_start_depth = -1; + g_record_event = {}; + Profiler::instance()->threadFilter()->init("enabled"); + Profiler::instance()->threadFilter()->clearActive(); + } + + void TearDown() override { + if (ProfiledThread::current() != nullptr) { + ProfiledThread::release(); + } + Profiler::setTaskBlockRecordOverrideForTest(nullptr); + Profiler::instance()->setTaskBlockEnabledForTest(false); + Profiler::instance()->threadFilter()->clearActive(); + Counters::reset(); + } + + int registerCurrentJavaThread(ProfiledThread* thread) { + ThreadFilter* filter = Profiler::instance()->threadFilter(); + int slot_id = filter->registerThread(); + EXPECT_GE(slot_id, 0); + thread->setJavaThread(true); + thread->setFilterSlotId(slot_id); + filter->add(thread->tid(), slot_id); + return slot_id; + } +}; + +u64 eligibleEndTicks(u64 start_ticks) { + return start_ticks + (TSC::frequency() / 1000) + 1; +} + +} // namespace + +TEST_F(NativeBlockScopeTest, DisabledTaskBlockGateLeavesScopeInactiveAndPreservesErrno) { + CurrentThreadScope current; + ScopedTaskBlockEnabled task_block_enabled(false); + + errno = E2BIG; + NativeBlockScope scope(NativeBlockKind::STREAM_SOCKET, 17); + + EXPECT_FALSE(scope.active()); + EXPECT_EQ(E2BIG, errno); +} + +TEST_F(NativeBlockScopeTest, NullCurrentThreadGateLeavesScopeInactiveAndPreservesErrno) { + CurrentThreadScope current; + ScopedTaskBlockEnabled task_block_enabled(true); + DetachedCurrentThread detached(current); + + errno = E2BIG; + NativeBlockScope scope(NativeBlockKind::STREAM_SOCKET, 17); + + EXPECT_FALSE(scope.active()); + EXPECT_EQ(E2BIG, errno); +} + +TEST_F(NativeBlockScopeTest, NonJavaThreadGateLeavesScopeInactiveAndPreservesErrno) { + CurrentThreadScope current; + ScopedTaskBlockEnabled task_block_enabled(true); + current.thread()->setJavaThread(false); + + errno = E2BIG; + NativeBlockScope scope(NativeBlockKind::STREAM_SOCKET, 17); + + EXPECT_FALSE(scope.active()); + EXPECT_EQ(E2BIG, errno); +} + +TEST_F(NativeBlockScopeTest, MissingSlotGateLeavesScopeInactiveAndPreservesErrno) { + CurrentThreadScope current; + ScopedTaskBlockEnabled task_block_enabled(true); + current.thread()->setJavaThread(true); + current.thread()->setFilterSlotId(-1); + + errno = E2BIG; + NativeBlockScope scope(NativeBlockKind::STREAM_SOCKET, 17); + + EXPECT_FALSE(scope.active()); + EXPECT_EQ(E2BIG, errno); +} + +TEST_F(NativeBlockScopeTest, AllThreadRegistryWorksWithoutLegacyContextFilter) { + CurrentThreadScope current; + ScopedTaskBlockEnabled task_block_enabled(true); + ThreadFilter* filter = Profiler::instance()->threadFilter(); + filter->init(nullptr, true); + int slot_id = filter->registerThread(current.thread()->tid()); + ASSERT_GE(slot_id, 0); + current.thread()->setJavaThread(true); + current.thread()->setFilterSlotId(slot_id); + ASSERT_FALSE(filter->enabled()); + ASSERT_TRUE(filter->registryActive()); + + NativeBlockScope scope(NativeBlockKind::STREAM_SOCKET, 17); + + EXPECT_TRUE(scope.active()); + scope.finishForTest(eligibleEndTicks(scope.startTicksForTest())); + ThreadFilter::Slot* slot = filter->slotForId(slot_id); + ASSERT_NE(nullptr, slot); + EXPECT_EQ(BlockRunOwner::NONE, slot->activeBlockOwner()); +} + +TEST_F(NativeBlockScopeTest, TraceContextGateLeavesScopeInactiveAndSlotUnowned) { + CurrentThreadScope current; + ScopedTaskBlockEnabled task_block_enabled(true); + int slot_id = registerCurrentJavaThread(current.thread()); + current.thread()->setContextForTest(0x1234, 0x5678); + + errno = E2BIG; + NativeBlockScope scope(NativeBlockKind::STREAM_SOCKET, 17); + + EXPECT_FALSE(scope.active()); + EXPECT_EQ(E2BIG, errno); + EXPECT_EQ(1, Counters::getCounter(TASK_BLOCK_SKIPPED_TRACE_CONTEXT)); + ThreadFilter::Slot* slot = Profiler::instance()->threadFilter()->slotForId(slot_id); + ASSERT_NE(nullptr, slot); + EXPECT_EQ(BlockRunOwner::NONE, slot->activeBlockOwner()); + EXPECT_EQ(OSThreadState::UNKNOWN, slot->activeBlockState()); +} + +TEST_F(NativeBlockScopeTest, EnterBlockedRunFailureLeavesExistingOwnerIntact) { + CurrentThreadScope current; + ScopedTaskBlockEnabled task_block_enabled(true); + int slot_id = registerCurrentJavaThread(current.thread()); + ThreadFilter* filter = Profiler::instance()->threadFilter(); + u64 token = filter->enterBlockedRun(slot_id, OSThreadState::CONDVAR_WAIT, + BlockRunOwner::JVMTI); + ASSERT_NE(0ULL, token); + + errno = E2BIG; + NativeBlockScope scope(NativeBlockKind::STREAM_SOCKET, 17); + + EXPECT_FALSE(scope.active()); + EXPECT_EQ(E2BIG, errno); + ThreadFilter::Slot* slot = filter->slotForId(slot_id); + ASSERT_NE(nullptr, slot); + EXPECT_EQ(BlockRunOwner::JVMTI, slot->activeBlockOwner()); + EXPECT_EQ(OSThreadState::CONDVAR_WAIT, slot->activeBlockState()); + EXPECT_TRUE(filter->exitBlockedRun(slot_id, ThreadFilter::tokenGeneration(token))); +} + +TEST_F(NativeBlockScopeTest, ActiveScopeExitsSlotAndRecordsSynchronousIoWaitEvent) { + CurrentThreadScope current; + ScopedTaskBlockEnabled task_block_enabled(true); + int slot_id = registerCurrentJavaThread(current.thread()); + + errno = E2BIG; + NativeBlockScope scope(NativeBlockKind::STREAM_SOCKET, 17, + OSThreadState::IO_WAIT); + ASSERT_TRUE(scope.active()); + ThreadFilter::Slot* active_slot = Profiler::instance()->threadFilter()->slotForId(slot_id); + ASSERT_NE(nullptr, active_slot); + EXPECT_EQ(E2BIG, errno); + scope.finishForTest(eligibleEndTicks(scope.startTicksForTest())); + + ThreadFilter::Slot* slot = Profiler::instance()->threadFilter()->slotForId(slot_id); + ASSERT_NE(nullptr, slot); + EXPECT_EQ(BlockRunOwner::NONE, slot->activeBlockOwner()); + EXPECT_EQ(OSThreadState::UNKNOWN, slot->activeBlockState()); + EXPECT_EQ(1, g_record_calls.load(std::memory_order_relaxed)); + EXPECT_EQ(current.thread()->tid(), g_record_tid); + EXPECT_EQ(nullptr, g_record_thread); + EXPECT_EQ(0, g_record_start_depth); + EXPECT_EQ(OSThreadState::IO_WAIT, g_record_event._observedBlockingState); + EXPECT_EQ(NativeBlockScope::blocker(NativeBlockKind::STREAM_SOCKET, 17), + g_record_event._blocker); +} + +TEST_F(NativeBlockScopeTest, FinishAfterTaskBlockDisableExitsWithoutRecording) { + CurrentThreadScope current; + ScopedTaskBlockEnabled task_block_enabled(true); + int slot_id = registerCurrentJavaThread(current.thread()); + + NativeBlockScope scope(NativeBlockKind::CONNECT, 19, OSThreadState::IO_WAIT); + ASSERT_TRUE(scope.active()); + Profiler::instance()->setTaskBlockEnabledForTest(false); + scope.finishForTest(eligibleEndTicks(scope.startTicksForTest())); + + EXPECT_EQ(0, g_record_calls.load(std::memory_order_relaxed)); + ThreadFilter::Slot* slot = + Profiler::instance()->threadFilter()->slotForId(slot_id); + ASSERT_NE(nullptr, slot); + EXPECT_EQ(BlockRunOwner::NONE, slot->activeBlockOwner()); + EXPECT_EQ(OSThreadState::UNKNOWN, slot->activeBlockState()); +} + +TEST_F(NativeBlockScopeTest, FinishAfterFilterDisableExitsWithoutRecording) { + CurrentThreadScope current; + ScopedTaskBlockEnabled task_block_enabled(true); + int slot_id = registerCurrentJavaThread(current.thread()); + ThreadFilter* filter = Profiler::instance()->threadFilter(); + + { + NativeBlockScope scope(NativeBlockKind::STREAM_SOCKET, 17, + OSThreadState::IO_WAIT); + ASSERT_TRUE(scope.active()); + filter->init(nullptr); + } + + ThreadFilter::Slot* slot = filter->slotForId(slot_id); + ASSERT_NE(nullptr, slot); + EXPECT_EQ(BlockRunOwner::NONE, slot->activeBlockOwner()); + EXPECT_EQ(OSThreadState::UNKNOWN, slot->activeBlockState()); + EXPECT_EQ(0, g_record_calls.load(std::memory_order_relaxed)); +} + +TEST_F(NativeBlockScopeTest, RotationRejectsFinishWithoutBlockingOrStranding) { + CurrentThreadScope current; + ScopedTaskBlockEnabled task_block_enabled(true); + int slot_id = registerCurrentJavaThread(current.thread()); + ThreadFilter* filter = Profiler::instance()->threadFilter(); + NativeBlockScope scope(NativeBlockKind::STREAM_SOCKET, 17, + OSThreadState::IO_WAIT); + ASSERT_TRUE(scope.active()); + + Profiler* profiler = Profiler::instance(); + profiler->beginTaskBlockRotationForTest(); + std::future result = std::async(std::launch::async, [&]() { + scope.finishForTest(eligibleEndTicks(scope.startTicksForTest())); + }); + + std::future_status status = result.wait_for(std::chrono::seconds(1)); + profiler->endTaskBlockRotationForTest(); + ASSERT_EQ(std::future_status::ready, status); + result.get(); + + ThreadFilter::Slot* slot = filter->slotForId(slot_id); + ASSERT_NE(nullptr, slot); + EXPECT_EQ(BlockRunOwner::NONE, slot->activeBlockOwner()); + EXPECT_EQ(OSThreadState::UNKNOWN, slot->activeBlockState()); + EXPECT_EQ(0, g_record_calls.load(std::memory_order_relaxed)); + EXPECT_EQ(1, Counters::getCounter(TASK_BLOCK_DROPPED_ROTATION)); +} + +TEST_F(NativeBlockScopeTest, ConcurrentScopeLifecyclePreservesSlotOwnership) { + CurrentThreadScope current; + ScopedTaskBlockEnabled task_block_enabled(true); + int slot_id = registerCurrentJavaThread(current.thread()); + ThreadFilter* filter = Profiler::instance()->threadFilter(); + std::atomic stop{false}; + std::atomic failures{0}; + + std::thread observer([&]() { + while (!stop.load(std::memory_order_acquire)) { + BlockRunSnapshot snapshot = filter->slotForId(slot_id)->snapshotBlockRun(); + if (snapshot.active && snapshot.owner != BlockRunOwner::NATIVE) { + failures.fetch_add(1, std::memory_order_relaxed); + } + std::this_thread::yield(); + } + }); + + for (int i = 0; i < 1000; i++) { + { + NativeBlockScope scope(NativeBlockKind::STREAM_SOCKET, 17, + OSThreadState::IO_WAIT); + ASSERT_TRUE(scope.active()); + } + ThreadFilter::Slot* slot = filter->slotForId(slot_id); + ASSERT_NE(nullptr, slot); + ASSERT_EQ(BlockRunOwner::NONE, slot->activeBlockOwner()); + ASSERT_EQ(OSThreadState::UNKNOWN, slot->activeBlockState()); + } + + stop.store(true, std::memory_order_release); + observer.join(); + EXPECT_EQ(0, failures.load(std::memory_order_relaxed)); +} + +#endif // __linux__ diff --git a/ddprof-lib/src/test/cpp/nativeSocketInterposer_ut.cpp b/ddprof-lib/src/test/cpp/nativeSocketInterposer_ut.cpp new file mode 100644 index 0000000000..5aaccb5691 --- /dev/null +++ b/ddprof-lib/src/test/cpp/nativeSocketInterposer_ut.cpp @@ -0,0 +1,2076 @@ +/* + * Copyright 2026 Datadog, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#if defined(__linux__) + +#include "libraries.h" +#include "libraryPatcher.h" +#include "nativeBlock.h" +#include "nativeFdClassifier.h" +#include "nativeSocketInterposer.h" +#include "nativeSocketSampler.h" +#include "os.h" +#include "profiler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +static const int kFdTypeCacheSizeForTest = 65536; +static const int kHighFdCacheSizeForTest = 4096; + +std::atomic g_send_calls{0}; +std::atomic g_sampler_send_calls{0}; +std::atomic g_recv_calls{0}; +std::atomic g_write_calls{0}; +std::atomic g_sampler_write_calls{0}; +std::atomic g_read_calls{0}; +std::atomic g_close_calls{0}; +std::atomic g_connect_calls{0}; +std::atomic g_accept_calls{0}; +std::atomic g_accept4_calls{0}; +std::atomic g_recvfrom_calls{0}; +std::atomic g_recvmsg_calls{0}; +std::atomic g_epoll_wait_calls{0}; +std::atomic g_epoll_pwait_calls{0}; +std::atomic g_poll_calls{0}; +std::atomic g_ppoll_calls{0}; +std::atomic g_select_calls{0}; +std::atomic g_pselect_calls{0}; +std::atomic g_fd_probe_calls{0}; +std::atomic g_fd_probe_rc{0}; +std::atomic g_fd_probe_errno{0}; +std::atomic g_fd_probe_so_type{0}; +std::atomic g_fd_probe_last_fd{0}; +std::atomic g_blocking_probe_started{false}; +std::atomic g_release_blocking_probe{false}; +std::atomic g_sequence{0}; +std::atomic g_raw_syscall_sequence{0}; +std::atomic g_taskblock_enter_sequence{0}; +std::atomic g_taskblock_exit_sequence{0}; +std::atomic g_sampler_record_sequence{0}; +std::atomic g_send_ret{0}; +std::atomic g_sampler_send_ret{0}; +std::atomic g_recv_ret{0}; +std::atomic g_write_ret{0}; +std::atomic g_sampler_write_ret{0}; +std::atomic g_read_ret{0}; +std::atomic g_close_ret{0}; +std::atomic g_close_errno{0}; +std::atomic g_connect_ret{0}; +std::atomic g_accept_ret{0}; +std::atomic g_accept4_ret{0}; +std::atomic g_recvfrom_ret{0}; +std::atomic g_recvmsg_ret{0}; +std::atomic g_epoll_wait_ret{0}; +std::atomic g_epoll_pwait_ret{0}; +std::atomic g_poll_ret{0}; +std::atomic g_ppoll_ret{0}; +std::atomic g_select_ret{0}; +std::atomic g_pselect_ret{0}; + +ssize_t stub_send(int, const void*, size_t, int) { + g_send_calls++; + return g_send_ret.load(); +} + +ssize_t sampler_stub_send(int, const void*, size_t, int) { + g_sampler_send_calls++; + return g_sampler_send_ret.load(); +} + +int stub_fd_probe(int, int *so_type, int *probe_errno) { + g_fd_probe_calls++; + *so_type = g_fd_probe_so_type.load(std::memory_order_acquire); + *probe_errno = g_fd_probe_errno.load(std::memory_order_acquire); + return g_fd_probe_rc.load(std::memory_order_acquire); +} + +int recording_fd_probe(int fd, int *so_type, int *probe_errno) { + g_fd_probe_last_fd.store(fd, std::memory_order_release); + return stub_fd_probe(fd, so_type, probe_errno); +} + +int blocking_stream_fd_probe(int, int *so_type, int *probe_errno) { + g_blocking_probe_started.store(true, std::memory_order_release); + while (!g_release_blocking_probe.load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + *so_type = SOCK_STREAM; + *probe_errno = 0; + return 0; +} + +class ScopedFdProbeOverride { +public: + explicit ScopedFdProbeOverride(NativeFdClassifier::ProbeOverride probe) { + NativeFdClassifier::setProbeOverrideForTest(probe); + } + ~ScopedFdProbeOverride() { + NativeFdClassifier::setProbeOverrideForTest(nullptr); + } +}; + +class ScopedTaskBlockEnabled { +public: + explicit ScopedTaskBlockEnabled(bool enabled) + : _saved(Profiler::instance()->setTaskBlockEnabledForTest(enabled)) {} + ~ScopedTaskBlockEnabled() { + Profiler::instance()->setTaskBlockEnabledForTest(_saved); + } + +private: + bool _saved; +}; + +class ScopedNativeSocketInterposerActive { +public: + explicit ScopedNativeSocketInterposerActive(bool active) + : _saved(NativeSocketInterposer::instance()->setActiveForTest(active)) {} + ~ScopedNativeSocketInterposerActive() { + NativeSocketInterposer::instance()->setActiveForTest(_saved); + } + +private: + bool _saved; +}; + +class ScopedNativeSocketSamplerActive { +public: + explicit ScopedNativeSocketSamplerActive(bool active) + : _saved(NativeSocketSampler::setActiveForTest(active)) {} + ~ScopedNativeSocketSamplerActive() { + NativeSocketSampler::setActiveForTest(_saved); + } + +private: + bool _saved; +}; + +class ScopedHookOwnerPid { +public: + explicit ScopedHookOwnerPid(pid_t pid) + : _saved(NativeSocketInterposer::setHookOwnerPidForTest(pid)) {} + ~ScopedHookOwnerPid() { + NativeSocketInterposer::setHookOwnerPidForTest(_saved); + } + +private: + pid_t _saved; +}; + +void expectJavaProfilerMark(void* fn_addr) { + CodeCache* lib = Libraries::instance()->findLibraryByAddress(fn_addr); + ASSERT_NE(nullptr, lib); + const char* name = nullptr; + lib->binarySearch(fn_addr, &name); + ASSERT_NE(nullptr, name); + EXPECT_EQ(MARK_JAVA_PROFILER, NativeFunc::read_mark(name)) << name; +} + +TEST(NativeSocketInterposerMarkTest, MarksEverySampledSocketHookLayer) { + Libraries::instance()->updateSymbols(false); + ASSERT_TRUE(NativeSocketInterposer::markProfilerHooks()); + + void* sampler_hooks[] = { + reinterpret_cast(NativeSocketSampler::send_hook), + reinterpret_cast(NativeSocketSampler::recv_hook), + reinterpret_cast(NativeSocketSampler::write_hook), + reinterpret_cast(NativeSocketSampler::read_hook), + }; + for (void* hook : sampler_hooks) { + expectJavaProfilerMark(hook); + } + + const NativeSocketInterposer::NativeIoHookSpec* specs = + NativeSocketInterposer::hookSpecs(); + for (int hook_index = NativeSocketInterposer::HOOK_SEND; + hook_index <= NativeSocketInterposer::HOOK_READ; hook_index++) { + expectJavaProfilerMark(specs[hook_index].hook); + expectJavaProfilerMark(specs[hook_index].fork_safe_hook); + } +} + +ssize_t stub_recv(int, void*, size_t, int) { + g_recv_calls++; + return g_recv_ret.load(); +} + +ssize_t stub_write(int, const void*, size_t) { + g_write_calls++; + g_raw_syscall_sequence.store(g_sequence.fetch_add(1) + 1, + std::memory_order_release); + return g_write_ret.load(); +} + +ssize_t sampler_stub_write(int, const void*, size_t) { + g_sampler_write_calls++; + g_raw_syscall_sequence.store(g_sequence.fetch_add(1) + 1, + std::memory_order_release); + return g_sampler_write_ret.load(); +} + +ssize_t stub_read(int, void*, size_t) { + g_read_calls++; + g_raw_syscall_sequence.store(g_sequence.fetch_add(1) + 1, + std::memory_order_release); + return g_read_ret.load(); +} + +void native_block_observer(const char* phase, NativeBlockKind, int) { + int sequence = g_sequence.fetch_add(1) + 1; + if (strcmp(phase, "enter") == 0) { + g_taskblock_enter_sequence.store(sequence, std::memory_order_release); + } else if (strcmp(phase, "exit") == 0) { + g_taskblock_exit_sequence.store(sequence, std::memory_order_release); + } +} + +void native_socket_sampler_observer(const char* phase, int, u8, ssize_t) { + if (strcmp(phase, "record") == 0) { + g_sampler_record_sequence.store(g_sequence.fetch_add(1) + 1, + std::memory_order_release); + } +} + +int stub_close(int) { + g_close_calls++; + errno = g_close_errno.load(); + return g_close_ret.load(); +} + +int stub_connect(int, const struct sockaddr*, socklen_t) { + g_connect_calls++; + return g_connect_ret.load(); +} + +int stub_accept(int, struct sockaddr*, socklen_t*) { + g_accept_calls++; + return g_accept_ret.load(); +} + +int stub_accept4(int, struct sockaddr*, socklen_t*, int) { + g_accept4_calls++; + return g_accept4_ret.load(); +} + +ssize_t stub_recvfrom(int, void*, size_t, int, struct sockaddr*, socklen_t*) { + g_recvfrom_calls++; + return g_recvfrom_ret.load(); +} + +ssize_t stub_recvmsg(int, struct msghdr*, int) { + g_recvmsg_calls++; + return g_recvmsg_ret.load(); +} + +int stub_epoll_wait(int, struct epoll_event*, int, int) { + g_epoll_wait_calls++; + return g_epoll_wait_ret.load(); +} + +int stub_epoll_pwait(int, struct epoll_event*, int, int, const sigset_t*) { + g_epoll_pwait_calls++; + return g_epoll_pwait_ret.load(); +} + +int stub_poll(struct pollfd*, nfds_t, int) { + g_poll_calls++; + return g_poll_ret.load(); +} + +int stub_ppoll(struct pollfd*, nfds_t, const struct timespec*, const sigset_t*) { + g_ppoll_calls++; + return g_ppoll_ret.load(); +} + +int stub_select(int, fd_set*, fd_set*, fd_set*, struct timeval*) { + g_select_calls++; + return g_select_ret.load(); +} + +int stub_pselect(int, fd_set*, fd_set*, fd_set*, const struct timespec*, + const sigset_t*) { + g_pselect_calls++; + return g_pselect_ret.load(); +} + +void setOriginalFunction(NativeSocketInterposer::NativeIoHookIndex hook, void* fn) { + ASSERT_TRUE(NativeSocketInterposer::setOriginalFunction(hook, fn)); +} + +class NativeSocketInterposerHookTest : public ::testing::Test { +protected: + NativeSocketInterposer::send_fn saved_send = nullptr; + NativeSocketInterposer::recv_fn saved_recv = nullptr; + NativeSocketInterposer::write_fn saved_write = nullptr; + NativeSocketInterposer::read_fn saved_read = nullptr; + NativeSocketSampler::send_fn saved_sampler_send = nullptr; + NativeSocketSampler::recv_fn saved_sampler_recv = nullptr; + NativeSocketSampler::write_fn saved_sampler_write = nullptr; + NativeSocketSampler::read_fn saved_sampler_read = nullptr; + bool saved_active = false; + + void SetUp() override { + NativeSocketInterposer::getOriginalFunctions(saved_send, saved_recv, saved_write, + saved_read); + NativeSocketSampler::getOriginalFunctions(saved_sampler_send, saved_sampler_recv, + saved_sampler_write, saved_sampler_read); + NativeSocketInterposer::setOriginalFunctions(stub_send, stub_recv, stub_write, + stub_read); + NativeSocketSampler::setOriginalFunctions(sampler_stub_send, stub_recv, + sampler_stub_write, stub_read); + setOriginalFunction(NativeSocketInterposer::HOOK_CLOSE, + reinterpret_cast(stub_close)); + setOriginalFunction(NativeSocketInterposer::HOOK_CONNECT, + reinterpret_cast(stub_connect)); + setOriginalFunction(NativeSocketInterposer::HOOK_ACCEPT, + reinterpret_cast(stub_accept)); + setOriginalFunction(NativeSocketInterposer::HOOK_ACCEPT4, + reinterpret_cast(stub_accept4)); + setOriginalFunction(NativeSocketInterposer::HOOK_RECVFROM, + reinterpret_cast(stub_recvfrom)); + setOriginalFunction(NativeSocketInterposer::HOOK_RECVMSG, + reinterpret_cast(stub_recvmsg)); + setOriginalFunction(NativeSocketInterposer::HOOK_EPOLL_WAIT, + reinterpret_cast(stub_epoll_wait)); + setOriginalFunction(NativeSocketInterposer::HOOK_EPOLL_PWAIT, + reinterpret_cast(stub_epoll_pwait)); + setOriginalFunction(NativeSocketInterposer::HOOK_POLL, + reinterpret_cast(stub_poll)); + setOriginalFunction(NativeSocketInterposer::HOOK_PPOLL, + reinterpret_cast(stub_ppoll)); + setOriginalFunction(NativeSocketInterposer::HOOK_SELECT, + reinterpret_cast(stub_select)); + setOriginalFunction(NativeSocketInterposer::HOOK_PSELECT, + reinterpret_cast(stub_pselect)); + saved_active = LibraryPatcher::_socket_active.load(std::memory_order_acquire); + LibraryPatcher::_socket_active.store(false, std::memory_order_release); + NativeSocketInterposer::instance()->clearFdTypeCache(); + NativeSocketSampler::instance()->clearFdCache(); + NativeSocketSampler::resetSocketProbeCountForTest(); + NativeBlockScope::setHookObserverForTest(nullptr); + NativeSocketSampler::setHookObserverForTest(nullptr); + g_send_calls = 0; + g_sampler_send_calls = 0; + g_recv_calls = 0; + g_write_calls = 0; + g_sampler_write_calls = 0; + g_read_calls = 0; + g_close_calls = 0; + g_connect_calls = 0; + g_accept_calls = 0; + g_accept4_calls = 0; + g_recvfrom_calls = 0; + g_recvmsg_calls = 0; + g_epoll_wait_calls = 0; + g_epoll_pwait_calls = 0; + g_poll_calls = 0; + g_ppoll_calls = 0; + g_select_calls = 0; + g_pselect_calls = 0; + g_fd_probe_calls = 0; + g_sequence = 0; + g_raw_syscall_sequence = 0; + g_taskblock_enter_sequence = 0; + g_taskblock_exit_sequence = 0; + g_sampler_record_sequence = 0; + g_send_ret = 0; + g_sampler_send_ret = 0; + g_recv_ret = 0; + g_write_ret = 0; + g_sampler_write_ret = 0; + g_read_ret = 0; + g_close_ret = 0; + g_close_errno = 0; + g_connect_ret = 0; + g_accept_ret = 0; + g_accept4_ret = 0; + g_recvfrom_ret = 0; + g_recvmsg_ret = 0; + g_epoll_wait_ret = 0; + g_epoll_pwait_ret = 0; + g_poll_ret = 0; + g_ppoll_ret = 0; + g_select_ret = 0; + g_pselect_ret = 0; + } + + void TearDown() override { + LibraryPatcher::_socket_active.store(saved_active, std::memory_order_release); + NativeSocketInterposer::setOriginalFunctions(saved_send, saved_recv, saved_write, + saved_read); + NativeSocketSampler::setOriginalFunctions(saved_sampler_send, saved_sampler_recv, + saved_sampler_write, saved_sampler_read); + setOriginalFunction(NativeSocketInterposer::HOOK_CLOSE, nullptr); + setOriginalFunction(NativeSocketInterposer::HOOK_CONNECT, nullptr); + setOriginalFunction(NativeSocketInterposer::HOOK_ACCEPT, nullptr); + setOriginalFunction(NativeSocketInterposer::HOOK_ACCEPT4, nullptr); + setOriginalFunction(NativeSocketInterposer::HOOK_RECVFROM, nullptr); + setOriginalFunction(NativeSocketInterposer::HOOK_RECVMSG, nullptr); + setOriginalFunction(NativeSocketInterposer::HOOK_EPOLL_WAIT, nullptr); + setOriginalFunction(NativeSocketInterposer::HOOK_EPOLL_PWAIT, nullptr); + setOriginalFunction(NativeSocketInterposer::HOOK_POLL, nullptr); + setOriginalFunction(NativeSocketInterposer::HOOK_PPOLL, nullptr); + setOriginalFunction(NativeSocketInterposer::HOOK_SELECT, nullptr); + setOriginalFunction(NativeSocketInterposer::HOOK_PSELECT, nullptr); + NativeSocketInterposer::instance()->clearFdTypeCache(); + NativeSocketSampler::instance()->clearFdCache(); + NativeSocketSampler::resetSocketProbeCountForTest(); + NativeBlockScope::setHookObserverForTest(nullptr); + NativeSocketSampler::setHookObserverForTest(nullptr); + } +}; + +class NativeSocketInterposerFdTest : public ::testing::Test { +protected: + void SetUp() override { + setOriginalFunction(NativeSocketInterposer::HOOK_CLOSE, + reinterpret_cast(::close)); + setOriginalFunction(NativeSocketInterposer::HOOK_DUP2, + reinterpret_cast(::dup2)); + setOriginalFunction(NativeSocketInterposer::HOOK_DUP3, nullptr); + NativeSocketInterposer::instance()->clearFdTypeCache(); + NativeSocketSampler::instance()->clearFdCache(); + } + + void TearDown() override { + setOriginalFunction(NativeSocketInterposer::HOOK_CLOSE, nullptr); + setOriginalFunction(NativeSocketInterposer::HOOK_DUP2, nullptr); + setOriginalFunction(NativeSocketInterposer::HOOK_DUP3, nullptr); + NativeSocketInterposer::instance()->clearFdTypeCache(); + NativeSocketSampler::instance()->clearFdCache(); + } + + int closeThroughHook(int fd) { + return NativeSocketInterposer::close_hook(fd); + } + + int dup2ThroughHook(int oldfd, int newfd) { + return NativeSocketInterposer::dup2_hook(oldfd, newfd); + } + + int dup3ThroughHook(int oldfd, int newfd, int flags) { + return NativeSocketInterposer::dup3_hook(oldfd, newfd, flags); + } + + int datagramSocketAtFd(int target_fd) { + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0 || fd == target_fd) { + return fd; + } + + int ret = ::dup2(fd, target_fd); + int saved_errno = errno; + ::close(fd); + errno = saved_errno; + return ret; + } +}; + +class LibraryPatcherImportTest : public ::testing::Test { +protected: + void SetUp() override { + LibraryPatcher::unpatch_socket_functions(); + } + + void TearDown() override { + LibraryPatcher::unpatch_socket_functions(); + cache.reset(); + } + + void initializeImports(size_t count) { + imports[0] = reinterpret_cast(stub_read); + imports[1] = reinterpret_cast(stub_write); + imports[2] = reinterpret_cast(stub_recv); + cache = std::make_unique("import-test", -1, + NO_MIN_ADDRESS, NO_MAX_ADDRESS, + nullptr, true); + for (size_t index = 0; index < count; index++) { + cache->addImport(&imports[index], "read"); + } + } + + std::unique_ptr cache; + void* imports[3] = {}; +}; + +} // namespace + +TEST_F(NativeSocketInterposerHookTest, InactiveHookForwardsWithoutChangingErrno) { + g_send_ret = 13; + char buf[8] = {}; + + errno = E2BIG; + ssize_t ret = NativeSocketInterposer::send_hook(0, buf, sizeof(buf), 0); + + EXPECT_EQ(13, ret); + EXPECT_EQ(1, g_send_calls.load()); + EXPECT_EQ(E2BIG, errno); +} + +TEST_F(NativeSocketInterposerHookTest, + OriginalFunctionCanBePublishedWhileHooksReadIt) { + constexpr int iterations = 100000; + std::atomic start{false}; + char buffer[1] = {}; + + std::thread reader([&]() { + while (!start.load(std::memory_order_acquire)) { + } + for (int index = 0; index < iterations; index++) { + NativeSocketInterposer::send_hook(0, buffer, sizeof(buffer), 0); + } + }); + + start.store(true, std::memory_order_release); + for (int index = 0; index < iterations; index++) { + setOriginalFunction( + NativeSocketInterposer::HOOK_SEND, + reinterpret_cast(index % 2 == 0 ? stub_send : sampler_stub_send)); + } + reader.join(); + + EXPECT_EQ(iterations, g_send_calls.load() + g_sampler_send_calls.load()); + setOriginalFunction(NativeSocketInterposer::HOOK_SEND, + reinterpret_cast(stub_send)); +} + +TEST_F(NativeSocketInterposerHookTest, ActiveNonSocketReadPreservesEntryErrno) { + int fds[2]; + ASSERT_EQ(0, pipe(fds)); + g_read_ret = 7; + char buf[8] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + ssize_t ret = NativeSocketInterposer::read_hook(fds[0], buf, sizeof(buf)); + + EXPECT_EQ(7, ret); + EXPECT_EQ(1, g_read_calls.load()); + EXPECT_EQ(E2BIG, errno); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, ActiveStreamSocketWriteForwards) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + g_write_ret = 5; + char buf[8] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + ssize_t ret = NativeSocketInterposer::write_hook(fds[0], buf, sizeof(buf)); + + EXPECT_EQ(5, ret); + EXPECT_EQ(1, g_write_calls.load()); + EXPECT_EQ(E2BIG, errno); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, + CombinedActiveStreamSendUsesSharedRawSyscall) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + ScopedNativeSocketInterposerActive interposer_active(true); + ScopedNativeSocketSamplerActive sampler_active(true); + g_send_ret = 11; + char buf[8] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + ssize_t ret = NativeSocketInterposer::send_hook(fds[0], buf, sizeof(buf), 0); + + EXPECT_EQ(11, ret); + EXPECT_EQ(1, g_send_calls.load()); + EXPECT_EQ(0, g_sampler_send_calls.load()); + EXPECT_EQ(E2BIG, errno); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, + CombinedActiveStreamWriteUsesSharedRawSyscall) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + ScopedNativeSocketInterposerActive interposer_active(true); + ScopedNativeSocketSamplerActive sampler_active(true); + g_write_ret = 11; + char buf[8] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + ssize_t ret = NativeSocketInterposer::write_hook(fds[0], buf, sizeof(buf)); + + EXPECT_EQ(11, ret); + EXPECT_EQ(1, g_write_calls.load()); + EXPECT_EQ(0, g_sampler_write_calls.load()); + EXPECT_EQ(E2BIG, errno); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, + SamplerOnlyStreamWriteClassifiesThroughSamplerClassifier) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + ScopedNativeSocketInterposerActive interposer_active(false); + ScopedNativeSocketSamplerActive sampler_active(true); + ScopedFdProbeOverride override(recording_fd_probe); + g_fd_probe_calls = 0; + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_STREAM; + g_sampler_write_ret = 23; + char buf[8] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + ssize_t ret = NativeSocketInterposer::write_hook(fds[0], buf, sizeof(buf)); + + EXPECT_EQ(23, ret); + EXPECT_EQ(0, g_write_calls.load()); + EXPECT_EQ(1, g_sampler_write_calls.load()); + EXPECT_EQ(1, g_fd_probe_calls.load()) + << "sampler-only path must classify through its NativeFdClassifier instance"; + EXPECT_EQ(E2BIG, errno); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, + CombinedActiveStreamWriteClassifiesOnceAndRecordsAfterTaskBlockScope) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + ScopedNativeSocketInterposerActive interposer_active(true); + ScopedNativeSocketSamplerActive sampler_active(true); + ScopedFdProbeOverride override(recording_fd_probe); + NativeBlockScope::setHookObserverForTest(native_block_observer); + NativeSocketSampler::setHookObserverForTest(native_socket_sampler_observer); + g_fd_probe_calls = 0; + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_STREAM; + g_write_ret = 31; + char buf[8] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + ssize_t ret = NativeSocketInterposer::write_hook(fds[0], buf, sizeof(buf)); + + EXPECT_EQ(31, ret); + EXPECT_EQ(1, g_write_calls.load()); + EXPECT_EQ(0, g_sampler_write_calls.load()); + EXPECT_EQ(1, g_fd_probe_calls.load()); + EXPECT_LT(0, g_taskblock_enter_sequence.load()); + EXPECT_LT(g_taskblock_enter_sequence.load(), g_raw_syscall_sequence.load()); + EXPECT_LT(g_raw_syscall_sequence.load(), g_taskblock_exit_sequence.load()); + EXPECT_LT(g_taskblock_exit_sequence.load(), g_sampler_record_sequence.load()); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, + ForkSafeOwnerProcessUsesInstrumentedHook) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + ScopedHookOwnerPid owner(getpid()); + ScopedNativeSocketInterposerActive interposer_active(true); + ScopedFdProbeOverride override(recording_fd_probe); + NativeBlockScope::setHookObserverForTest(native_block_observer); + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_STREAM; + g_write_ret = 37; + char buf[8] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + ssize_t ret = NativeSocketInterposer::fork_safe_write_hook( + fds[0], buf, sizeof(buf)); + + EXPECT_EQ(37, ret); + EXPECT_EQ(1, g_write_calls.load()); + EXPECT_EQ(1, g_fd_probe_calls.load()); + EXPECT_LT(0, g_taskblock_enter_sequence.load()); + EXPECT_LT(g_taskblock_enter_sequence.load(), g_raw_syscall_sequence.load()); + EXPECT_LT(g_raw_syscall_sequence.load(), g_taskblock_exit_sequence.load()); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, + ForkSafeChildBypassesProfilerState) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + ScopedHookOwnerPid owner(getpid() + 1); + ScopedNativeSocketInterposerActive interposer_active(true); + ScopedNativeSocketSamplerActive sampler_active(true); + ScopedFdProbeOverride override(recording_fd_probe); + NativeBlockScope::setHookObserverForTest(native_block_observer); + NativeSocketSampler::setHookObserverForTest(native_socket_sampler_observer); + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_STREAM; + g_send_ret = 41; + char buf[8] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + ssize_t ret = NativeSocketInterposer::fork_safe_send_hook( + fds[0], buf, sizeof(buf), 0); + + EXPECT_EQ(41, ret); + EXPECT_EQ(1, g_send_calls.load()); + EXPECT_EQ(0, g_sampler_send_calls.load()); + EXPECT_EQ(0, g_fd_probe_calls.load()); + EXPECT_EQ(0, g_taskblock_enter_sequence.load()); + EXPECT_EQ(0, g_taskblock_exit_sequence.load()); + EXPECT_EQ(0, g_sampler_record_sequence.load()); + EXPECT_EQ(E2BIG, errno); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, + ForkSafeChildReadAndPollBypassProfilerState) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + ScopedHookOwnerPid owner(getpid() + 1); + ScopedNativeSocketInterposerActive interposer_active(true); + ScopedFdProbeOverride override(recording_fd_probe); + NativeBlockScope::setHookObserverForTest(native_block_observer); + g_read_ret = 47; + g_poll_ret = 3; + char buf[8] = {}; + struct pollfd poll_fd = {fds[0], POLLIN, 0}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + EXPECT_EQ(47, NativeSocketInterposer::fork_safe_read_hook( + fds[0], buf, sizeof(buf))); + EXPECT_EQ(3, NativeSocketInterposer::fork_safe_poll_hook(&poll_fd, 1, 10)); + + EXPECT_EQ(1, g_read_calls.load()); + EXPECT_EQ(1, g_poll_calls.load()); + EXPECT_EQ(0, g_fd_probe_calls.load()); + EXPECT_EQ(0, g_taskblock_enter_sequence.load()); + EXPECT_EQ(0, g_taskblock_exit_sequence.load()); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, + ForkSafeChildNullOriginalReturnsEnosysWithoutInstrumentation) { + ScopedHookOwnerPid owner(getpid() + 1); + setOriginalFunction(NativeSocketInterposer::HOOK_SEND, nullptr); + char buf[8] = {}; + + errno = 0; + ssize_t ret = NativeSocketInterposer::fork_safe_send_hook( + -1, buf, sizeof(buf), 0); + + EXPECT_EQ(-1, ret); + EXPECT_EQ(ENOSYS, errno); + EXPECT_EQ(0, g_send_calls.load()); + EXPECT_EQ(0, g_fd_probe_calls.load()); +} + +TEST_F(NativeSocketInterposerHookTest, + ForkSafeChildCloseDoesNotClearProfilerCaches) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + ScopedHookOwnerPid owner(getpid() + 1); + ScopedFdProbeOverride override(recording_fd_probe); + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_STREAM; + g_close_ret = 0; + g_close_errno = E2BIG; + + EXPECT_TRUE(NativeSocketInterposer::instance()->isStreamSocket(fds[0])); + NativeSocketSampler::instance()->fdAddrCacheInsertForTest(fds[0], "cached"); + g_fd_probe_calls = 0; + errno = ERANGE; + int ret = NativeSocketInterposer::fork_safe_close_hook(fds[0]); + + EXPECT_EQ(0, ret); + EXPECT_EQ(1, g_close_calls.load()); + EXPECT_EQ(E2BIG, errno); + EXPECT_TRUE(NativeSocketSampler::instance()->fdAddrCacheContainsForTest(fds[0])); + EXPECT_TRUE(NativeSocketInterposer::instance()->isStreamSocket(fds[0])); + EXPECT_EQ(0, g_fd_probe_calls.load()); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, ForkSafeHookDetectsRealForkChild) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + ScopedHookOwnerPid owner(getpid()); + ScopedNativeSocketInterposerActive interposer_active(true); + g_send_ret = 43; + char buf[8] = {}; + + pid_t child = fork(); + ASSERT_GE(child, 0); + if (child == 0) { + ssize_t ret = NativeSocketInterposer::fork_safe_send_hook( + fds[0], buf, sizeof(buf), 0); + _exit(ret == 43 ? 0 : 1); + } + + int status = 0; + ASSERT_EQ(child, waitpid(child, &status, 0)); + EXPECT_TRUE(WIFEXITED(status)); + EXPECT_EQ(0, WEXITSTATUS(status)); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, CloseForwardsAndPreservesErrno) { + int fds[2]; + ASSERT_EQ(0, pipe(fds)); + g_close_ret = 0; + g_close_errno = E2BIG; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = ERANGE; + int ret = NativeSocketInterposer::close_hook(fds[0]); + + EXPECT_EQ(0, ret); + EXPECT_EQ(1, g_close_calls.load()); + EXPECT_EQ(E2BIG, errno); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, NullStreamSendOriginalReturnsEnosys) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + setOriginalFunction(NativeSocketInterposer::HOOK_SEND, nullptr); + char buf[8] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = 0; + ssize_t ret = NativeSocketInterposer::send_hook(fds[0], buf, sizeof(buf), 0); + + EXPECT_EQ(-1, ret); + EXPECT_EQ(ENOSYS, errno); + EXPECT_EQ(0, g_send_calls.load()); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, NullStreamRecvOriginalReturnsEnosys) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + setOriginalFunction(NativeSocketInterposer::HOOK_RECV, nullptr); + char buf[8] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = 0; + ssize_t ret = NativeSocketInterposer::recv_hook(fds[0], buf, sizeof(buf), 0); + + EXPECT_EQ(-1, ret); + EXPECT_EQ(ENOSYS, errno); + EXPECT_EQ(0, g_recv_calls.load()); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, NullStreamWriteOriginalReturnsEnosys) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + setOriginalFunction(NativeSocketInterposer::HOOK_WRITE, nullptr); + char buf[8] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = 0; + ssize_t ret = NativeSocketInterposer::write_hook(fds[0], buf, sizeof(buf)); + + EXPECT_EQ(-1, ret); + EXPECT_EQ(ENOSYS, errno); + EXPECT_EQ(0, g_write_calls.load()); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, NullStreamReadOriginalReturnsEnosys) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + setOriginalFunction(NativeSocketInterposer::HOOK_READ, nullptr); + char buf[8] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = 0; + ssize_t ret = NativeSocketInterposer::read_hook(fds[0], buf, sizeof(buf)); + + EXPECT_EQ(-1, ret); + EXPECT_EQ(ENOSYS, errno); + EXPECT_EQ(0, g_read_calls.load()); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerFdTest, CloseFallsBackToSyscallWhenOriginalIsMissing) { + int fds[2]; + ASSERT_EQ(0, pipe(fds)); + setOriginalFunction(NativeSocketInterposer::HOOK_CLOSE, nullptr); + + errno = E2BIG; + int ret = closeThroughHook(fds[0]); + + EXPECT_EQ(0, ret); + EXPECT_EQ(E2BIG, errno); + errno = 0; + EXPECT_EQ(-1, close(fds[0])); + EXPECT_EQ(EBADF, errno); + close(fds[1]); +} + +TEST(LibraryPatcherSocketStateTest, ConditionalUnpatchClearsSocketActiveWhenOwnersInactive) { + bool saved_active = + LibraryPatcher::_socket_active.exchange(true, std::memory_order_acq_rel); + + EXPECT_TRUE(LibraryPatcher::unpatch_socket_functions_if_inactive()); + EXPECT_FALSE(LibraryPatcher::_socket_active.load(std::memory_order_acquire)); + + LibraryPatcher::_socket_active.store(saved_active, std::memory_order_release); +} + +TEST(LibraryPatcherSocketStateTest, ConditionalUnpatchKeepsSocketActiveWhenSamplerActive) { + bool saved_active = + LibraryPatcher::_socket_active.exchange(true, std::memory_order_acq_rel); + ScopedNativeSocketSamplerActive sampler_active(true); + + EXPECT_FALSE(LibraryPatcher::unpatch_socket_functions_if_inactive()); + EXPECT_TRUE(LibraryPatcher::_socket_active.load(std::memory_order_acquire)); + + LibraryPatcher::_socket_active.store(saved_active, std::memory_order_release); +} + +TEST(LibraryPatcherSocketStateTest, ConditionalUnpatchKeepsSocketActiveWhenInterposerActive) { + bool saved_active = + LibraryPatcher::_socket_active.exchange(true, std::memory_order_acq_rel); + ScopedNativeSocketInterposerActive interposer_active(true); + + EXPECT_FALSE(LibraryPatcher::unpatch_socket_functions_if_inactive()); + EXPECT_TRUE(LibraryPatcher::_socket_active.load(std::memory_order_acquire)); + + LibraryPatcher::_socket_active.store(saved_active, std::memory_order_release); +} + +TEST(LibraryPatcherSocketStateTest, + RefreshDoesNotPatchAfterSocketHooksBecomeInactive) { + LibraryPatcher::unpatch_socket_functions(); + + EXPECT_FALSE(LibraryPatcher::patch_socket_functions(true)); + EXPECT_FALSE(LibraryPatcher::_socket_active.load(std::memory_order_acquire)); + EXPECT_EQ(0, LibraryPatcher::socket_patch_count_for_test()); + EXPECT_EQ(0, LibraryPatcher::socket_library_count_for_test()); +} + +TEST(LibraryPatcherSocketStateTest, + RestrictsSocketHooksToJdkNetworkingLibraries) { + CodeCache standard("libnet.so"); + CodeCache openjdk_java("libjava.so"); + CodeCache partial_ibm_java("libjava.so"); + CodeCache ibm_java("libjava.so"); + CodeCache marked_other("libother.so"); + + char marker_addresses[4] = {}; + partial_ibm_java.add(&marker_addresses[0], 1, "JCL_Send"); + const char* markers[] = { + "JCL_Send", "JCL_Recv", "JCL_Connect", "JCL_Accept"}; + for (size_t index = 0; index < 4; index++) { + ibm_java.add(&marker_addresses[index], 1, markers[index]); + marked_other.add(&marker_addresses[index], 1, markers[index]); + } + + EXPECT_EQ(SOCKET_PATCH_STANDARD_JDK_NETWORK, + LibraryPatcher::socket_patch_target_for_test( + &standard, "libnet.so", true)); + EXPECT_EQ(SOCKET_PATCH_STANDARD_JDK_NETWORK, + LibraryPatcher::socket_patch_target_for_test( + &standard, "libnio.so", true)); + + EXPECT_EQ(SOCKET_PATCH_NONE, + LibraryPatcher::socket_patch_target_for_test( + &openjdk_java, "libjava.so", true)); + EXPECT_EQ(SOCKET_PATCH_NONE, + LibraryPatcher::socket_patch_target_for_test( + &partial_ibm_java, "libjava.so", true)); + EXPECT_EQ(SOCKET_PATCH_IBM_JCL_BRIDGE, + LibraryPatcher::socket_patch_target_for_test( + &ibm_java, "libjava.so", true)); + EXPECT_EQ(SOCKET_PATCH_NONE, + LibraryPatcher::socket_patch_target_for_test( + &marked_other, "libother.so", true)); + EXPECT_EQ(SOCKET_PATCH_NONE, + LibraryPatcher::socket_patch_target_for_test( + &ibm_java, "libjava.so", false)); + EXPECT_EQ(SOCKET_PATCH_NONE, + LibraryPatcher::socket_patch_target_for_test( + &standard, "libnative-plugin.so", true)); + EXPECT_EQ(SOCKET_PATCH_NONE, + LibraryPatcher::socket_patch_target_for_test( + &standard, "libnet.so.backup", true)); + EXPECT_EQ(SOCKET_PATCH_NONE, + LibraryPatcher::socket_patch_target_for_test( + &standard, "libnet.so", false)); + EXPECT_EQ(SOCKET_PATCH_NONE, + LibraryPatcher::socket_patch_target_for_test( + &standard, nullptr, true)); + EXPECT_EQ(SOCKET_PATCH_NONE, + LibraryPatcher::socket_patch_target_for_test( + nullptr, "libnet.so", true)); + + void* standard_hook = LibraryPatcher::socket_hook_for_target_for_test( + SOCKET_PATCH_STANDARD_JDK_NETWORK, NativeSocketInterposer::HOOK_SEND); + void* ibm_hook = LibraryPatcher::socket_hook_for_target_for_test( + SOCKET_PATCH_IBM_JCL_BRIDGE, NativeSocketInterposer::HOOK_SEND); + EXPECT_EQ(reinterpret_cast(NativeSocketInterposer::send_hook), + standard_hook); + EXPECT_EQ(reinterpret_cast(NativeSocketInterposer::fork_safe_send_hook), + ibm_hook); + EXPECT_NE(standard_hook, ibm_hook); + for (int hook_index = 0; + hook_index < NativeSocketInterposer::NUM_NATIVE_IO_HOOKS; hook_index++) { + void* regular = LibraryPatcher::socket_hook_for_target_for_test( + SOCKET_PATCH_STANDARD_JDK_NETWORK, hook_index); + void* fork_safe = LibraryPatcher::socket_hook_for_target_for_test( + SOCKET_PATCH_IBM_JCL_BRIDGE, hook_index); + EXPECT_NE(nullptr, regular) << hook_index; + EXPECT_NE(nullptr, fork_safe) << hook_index; + EXPECT_NE(regular, fork_safe) << hook_index; + } +} + +TEST_F(LibraryPatcherImportTest, PatchesAndRestoresEveryImportLocation) { + initializeImports(3); + void* originals[3] = {imports[0], imports[1], imports[2]}; + void* hook = reinterpret_cast(NativeSocketInterposer::read_hook); + + EXPECT_EQ(3, LibraryPatcher::patch_socket_import_for_test( + cache.get(), im_read, hook, "read")); + EXPECT_EQ(3, LibraryPatcher::socket_patch_count_for_test()); + EXPECT_EQ(hook, imports[0]); + EXPECT_EQ(hook, imports[1]); + EXPECT_EQ(hook, imports[2]); + + EXPECT_EQ(0, LibraryPatcher::patch_socket_import_for_test( + cache.get(), im_read, hook, "read")); + EXPECT_EQ(3, LibraryPatcher::socket_patch_count_for_test()); + + LibraryPatcher::unpatch_socket_functions(); + EXPECT_EQ(originals[0], imports[0]); + EXPECT_EQ(originals[1], imports[1]); + EXPECT_EQ(originals[2], imports[2]); + EXPECT_EQ(0, LibraryPatcher::socket_patch_count_for_test()); +} + +TEST_F(LibraryPatcherImportTest, PatchesSingleImportLocation) { + initializeImports(1); + void* original = imports[0]; + void* hook = reinterpret_cast(NativeSocketInterposer::read_hook); + + EXPECT_EQ(1, LibraryPatcher::patch_socket_import_for_test( + cache.get(), im_read, hook, "read")); + EXPECT_EQ(hook, imports[0]); + EXPECT_EQ(1, LibraryPatcher::socket_patch_count_for_test()); + + LibraryPatcher::unpatch_socket_functions(); + EXPECT_EQ(original, imports[0]); +} + +TEST_F(LibraryPatcherImportTest, MissingImportDoesNotConsumePatchSlot) { + initializeImports(1); + void* hook = reinterpret_cast(NativeSocketInterposer::send_hook); + + EXPECT_EQ(0, LibraryPatcher::patch_socket_import_for_test( + cache.get(), im_send, hook, "send")); + EXPECT_EQ(0, LibraryPatcher::socket_patch_count_for_test()); +} + +TEST(LibraryPatcherDsoLifetimeTest, RetainsPatchedLibraryUntilImportsAreRestored) { + // Gradle starts gtests from ddprof-lib, while GitLab runs the binaries from + // the repository root. The support library is under the root build directory + // in both cases. + const char* paths[] = { + "build/test/resources/native-libs/unloadable-io-lib/" + "libunloadable-io.so", + "../build/test/resources/native-libs/unloadable-io-lib/" + "libunloadable-io.so", + }; + void* handle = nullptr; + for (const char* path : paths) { + handle = dlopen(path, RTLD_NOW | RTLD_LOCAL); + if (handle != nullptr) { + break; + } + } + ASSERT_NE(nullptr, handle) << dlerror(); + void* symbol = dlsym(handle, "unloadable_read"); + ASSERT_NE(nullptr, symbol) << dlerror(); + + Libraries::instance()->updateSymbols(false); + CodeCache* lib = + Libraries::instance()->findLibraryByName("libunloadable-io"); + ASSERT_NE(nullptr, lib); + ASSERT_EQ(1u, lib->importCount(im_read)); + + LibraryPatcher::unpatch_socket_functions(); + EXPECT_EQ(1, LibraryPatcher::patch_socket_import_for_test( + lib, im_read, + reinterpret_cast(NativeSocketInterposer::read_hook), + "read", true)); + EXPECT_EQ(1, LibraryPatcher::socket_library_count_for_test()); + + ASSERT_EQ(0, dlclose(handle)); + Dl_info info; + EXPECT_NE(0, dladdr(symbol, &info)); + + LibraryPatcher::unpatch_socket_functions(); + EXPECT_EQ(0, LibraryPatcher::socket_patch_count_for_test()); + EXPECT_EQ(0, LibraryPatcher::socket_library_count_for_test()); + if (OS::isMusl()) { + EXPECT_NE(0, dladdr(symbol, &info)); + } else { + EXPECT_EQ(0, dladdr(symbol, &info)); + } +} + +TEST_F(NativeSocketInterposerHookTest, ActiveStreamSocketConnectForwards) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + g_connect_ret = 0; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + int ret = NativeSocketInterposer::connect_hook(fds[0], nullptr, 0); + + EXPECT_EQ(0, ret); + EXPECT_EQ(1, g_connect_calls.load()); + EXPECT_EQ(E2BIG, errno); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, ActiveStreamSocketAcceptForwards) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + g_accept_ret = 17; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + int ret = NativeSocketInterposer::accept_hook(fds[0], nullptr, nullptr); + + EXPECT_EQ(17, ret); + EXPECT_EQ(1, g_accept_calls.load()); + EXPECT_EQ(E2BIG, errno); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, ActiveStreamSocketAccept4Forwards) { + int fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds)); + g_accept4_ret = 19; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + int ret = NativeSocketInterposer::accept4_hook(fds[0], nullptr, nullptr, 0); + + EXPECT_EQ(19, ret); + EXPECT_EQ(1, g_accept4_calls.load()); + EXPECT_EQ(E2BIG, errno); + close(fds[0]); + close(fds[1]); +} + +TEST_F(NativeSocketInterposerHookTest, ActiveDatagramRecvfromForwards) { + int fd = socket(AF_INET, SOCK_DGRAM, 0); + ASSERT_GE(fd, 0); + g_recvfrom_ret = 3; + char buf[8] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + ssize_t ret = NativeSocketInterposer::recvfrom_hook(fd, buf, sizeof(buf), 0, + nullptr, nullptr); + + EXPECT_EQ(3, ret); + EXPECT_EQ(1, g_recvfrom_calls.load()); + EXPECT_EQ(E2BIG, errno); + close(fd); +} + +TEST_F(NativeSocketInterposerHookTest, ActiveDatagramRecvmsgForwards) { + int fd = socket(AF_INET, SOCK_DGRAM, 0); + ASSERT_GE(fd, 0); + g_recvmsg_ret = 4; + struct msghdr msg = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + ssize_t ret = NativeSocketInterposer::recvmsg_hook(fd, &msg, 0); + + EXPECT_EQ(4, ret); + EXPECT_EQ(1, g_recvmsg_calls.load()); + EXPECT_EQ(E2BIG, errno); + close(fd); +} + +TEST_F(NativeSocketInterposerHookTest, ActiveEpollZeroTimeoutForwards) { + g_epoll_wait_ret = 0; + struct epoll_event events[1] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + int ret = NativeSocketInterposer::epoll_wait_hook(31, events, 1, 0); + + EXPECT_EQ(0, ret); + EXPECT_EQ(1, g_epoll_wait_calls.load()); + EXPECT_EQ(E2BIG, errno); +} + +TEST_F(NativeSocketInterposerHookTest, ActiveEpollPwaitZeroTimeoutForwards) { + g_epoll_pwait_ret = 0; + struct epoll_event events[1] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + int ret = NativeSocketInterposer::epoll_pwait_hook(31, events, 1, 0, nullptr); + + EXPECT_EQ(0, ret); + EXPECT_EQ(1, g_epoll_pwait_calls.load()); + EXPECT_EQ(E2BIG, errno); +} + +TEST_F(NativeSocketInterposerHookTest, ActivePollZeroTimeoutForwards) { + g_poll_ret = 0; + struct pollfd fds[1] = {{0, POLLIN, 0}}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + int ret = NativeSocketInterposer::poll_hook(fds, 1, 0); + + EXPECT_EQ(0, ret); + EXPECT_EQ(1, g_poll_calls.load()); + EXPECT_EQ(E2BIG, errno); +} + +TEST_F(NativeSocketInterposerHookTest, ActivePpollZeroTimeoutForwards) { + g_ppoll_ret = 0; + struct pollfd fds[1] = {{0, POLLIN, 0}}; + struct timespec timeout = {0, 0}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + int ret = NativeSocketInterposer::ppoll_hook(fds, 1, &timeout, nullptr); + + EXPECT_EQ(0, ret); + EXPECT_EQ(1, g_ppoll_calls.load()); + EXPECT_EQ(E2BIG, errno); +} + +TEST_F(NativeSocketInterposerHookTest, ActiveSelectZeroTimeoutForwards) { + g_select_ret = 0; + struct timeval timeout = {0, 0}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + int ret = NativeSocketInterposer::select_hook(1, nullptr, nullptr, nullptr, + &timeout); + + EXPECT_EQ(0, ret); + EXPECT_EQ(1, g_select_calls.load()); + EXPECT_EQ(E2BIG, errno); +} + +TEST_F(NativeSocketInterposerHookTest, ActivePselectZeroTimeoutForwards) { + g_pselect_ret = 0; + struct timespec timeout = {0, 0}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + int ret = NativeSocketInterposer::pselect_hook(1, nullptr, nullptr, nullptr, + &timeout, nullptr); + + EXPECT_EQ(0, ret); + EXPECT_EQ(1, g_pselect_calls.load()); + EXPECT_EQ(E2BIG, errno); +} + +TEST_F(NativeSocketInterposerHookTest, ActiveEpollPositiveTimeoutEligibleForwards) { + g_epoll_wait_ret = 1; + g_epoll_pwait_ret = 2; + struct epoll_event events[1] = {}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + EXPECT_EQ(1, NativeSocketInterposer::epoll_wait_hook(31, events, 1, 1)); + EXPECT_EQ(E2BIG, errno); + EXPECT_EQ(2, NativeSocketInterposer::epoll_pwait_hook(31, events, 1, -1, nullptr)); + EXPECT_EQ(E2BIG, errno); + EXPECT_EQ(1, g_epoll_wait_calls.load()); + EXPECT_EQ(1, g_epoll_pwait_calls.load()); +} + +TEST_F(NativeSocketInterposerHookTest, ActivePollPositiveAndNullTimeoutEligibleForwards) { + g_poll_ret = 1; + g_ppoll_ret = 2; + struct pollfd fds[1] = {{0, POLLIN, 0}}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + EXPECT_EQ(1, NativeSocketInterposer::poll_hook(fds, 1, -1)); + EXPECT_EQ(E2BIG, errno); + EXPECT_EQ(2, NativeSocketInterposer::ppoll_hook(fds, 1, nullptr, nullptr)); + EXPECT_EQ(E2BIG, errno); + EXPECT_EQ(1, g_poll_calls.load()); + EXPECT_EQ(1, g_ppoll_calls.load()); +} + +TEST_F(NativeSocketInterposerHookTest, ActiveSelectPositiveAndNullTimeoutEligibleForwards) { + g_select_ret = 1; + g_pselect_ret = 2; + struct timeval select_timeout = {1, 0}; + + LibraryPatcher::_socket_active.store(true, std::memory_order_release); + errno = E2BIG; + EXPECT_EQ(1, NativeSocketInterposer::select_hook(1, nullptr, nullptr, nullptr, + &select_timeout)); + EXPECT_EQ(E2BIG, errno); + EXPECT_EQ(2, NativeSocketInterposer::pselect_hook(1, nullptr, nullptr, nullptr, + nullptr, nullptr)); + EXPECT_EQ(E2BIG, errno); + EXPECT_EQ(1, g_select_calls.load()); + EXPECT_EQ(1, g_pselect_calls.load()); +} + +TEST(NativeBlockScopeTest, EncodesKindAndBlockerId) { + EXPECT_EQ((static_cast(NativeBlockKind::CONNECT) << 32) | 17, + NativeBlockScope::blocker(NativeBlockKind::CONNECT, 17)); +} + +TEST(NativeBlockScopeTest, DisabledTaskBlockGateLeavesScopeInactiveAndPreservesErrno) { + ScopedTaskBlockEnabled task_block_enabled(false); + + errno = E2BIG; + NativeBlockScope scope(NativeBlockKind::STREAM_SOCKET, 17); + + EXPECT_FALSE(scope.active()); + EXPECT_EQ(E2BIG, errno); +} + +TEST_F(NativeSocketInterposerFdTest, ClassifiesStreamSocketsOnly) { + int stream_fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds)); + EXPECT_TRUE(NativeSocketInterposer::instance()->isStreamSocket(stream_fds[0])); + ASSERT_EQ(0, closeThroughHook(stream_fds[0])); + ASSERT_EQ(0, closeThroughHook(stream_fds[1])); + + int datagram_fd = socket(AF_INET, SOCK_DGRAM, 0); + ASSERT_GE(datagram_fd, 0); + EXPECT_FALSE(NativeSocketInterposer::instance()->isStreamSocket(datagram_fd)); + ASSERT_EQ(0, closeThroughHook(datagram_fd)); +} + +TEST_F(NativeSocketInterposerFdTest, ClassifiesDatagramSocketsOnly) { + int datagram_fd = socket(AF_INET, SOCK_DGRAM, 0); + ASSERT_GE(datagram_fd, 0); + EXPECT_TRUE(NativeSocketInterposer::instance()->isDatagramSocket(datagram_fd)); + EXPECT_FALSE(NativeSocketInterposer::instance()->isStreamSocket(datagram_fd)); + ASSERT_EQ(0, closeThroughHook(datagram_fd)); + + int stream_fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds)); + EXPECT_FALSE(NativeSocketInterposer::instance()->isDatagramSocket(stream_fds[0])); + ASSERT_EQ(0, closeThroughHook(stream_fds[0])); + ASSERT_EQ(0, closeThroughHook(stream_fds[1])); +} + +TEST_F(NativeSocketInterposerFdTest, TransientFdProbeFailureIsNotCached) { + NativeFdClassifier classifier; + ScopedFdProbeOverride override(stub_fd_probe); + g_fd_probe_calls = 0; + g_fd_probe_rc = -1; + g_fd_probe_errno = EIO; + g_fd_probe_so_type = 0; + int fd = 42; + + EXPECT_FALSE(classifier.isStreamSocket(fd)); + EXPECT_EQ(1, g_fd_probe_calls.load()); + + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_STREAM; + + EXPECT_TRUE(classifier.isStreamSocket(fd)); + EXPECT_EQ(2, g_fd_probe_calls.load()); + + EXPECT_TRUE(classifier.isStreamSocket(fd)); + EXPECT_EQ(2, g_fd_probe_calls.load()); +} + +TEST_F(NativeSocketInterposerFdTest, NegativeFdDoesNotProbe) { + NativeFdClassifier classifier; + ScopedFdProbeOverride override(recording_fd_probe); + g_fd_probe_calls = 0; + g_fd_probe_so_type = SOCK_STREAM; + + EXPECT_FALSE(classifier.isStreamSocket(-1)); + EXPECT_FALSE(classifier.isDatagramSocket(-1)); + EXPECT_EQ(0, g_fd_probe_calls.load()); +} + +TEST_F(NativeSocketInterposerFdTest, EnotsockFailureIsCachedAsNonSocket) { + NativeFdClassifier classifier; + ScopedFdProbeOverride override(stub_fd_probe); + g_fd_probe_calls = 0; + g_fd_probe_rc = -1; + g_fd_probe_errno = ENOTSOCK; + g_fd_probe_so_type = 0; + + EXPECT_FALSE(classifier.isStreamSocket(43)); + EXPECT_FALSE(classifier.isDatagramSocket(43)); + EXPECT_EQ(1, g_fd_probe_calls.load()); +} + +TEST_F(NativeSocketInterposerFdTest, CacheNonSocketOverridesCachedStreamType) { + NativeFdClassifier classifier; + ScopedFdProbeOverride override(stub_fd_probe); + g_fd_probe_calls = 0; + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_STREAM; + + ASSERT_TRUE(classifier.isStreamSocket(43)); + EXPECT_EQ(1, g_fd_probe_calls.load()); + + classifier.cacheNonSocket(43); + + EXPECT_FALSE(classifier.isStreamSocket(43)); + EXPECT_FALSE(classifier.isDatagramSocket(43)); + EXPECT_EQ(1, g_fd_probe_calls.load()); +} + +TEST_F(NativeSocketInterposerFdTest, OtherSocketTypeIsCachedAsNeitherStreamNorDatagram) { + NativeFdClassifier classifier; + ScopedFdProbeOverride override(stub_fd_probe); + g_fd_probe_calls = 0; + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_RAW; + + EXPECT_FALSE(classifier.isStreamSocket(44)); + EXPECT_FALSE(classifier.isDatagramSocket(44)); + EXPECT_EQ(1, g_fd_probe_calls.load()); +} + +TEST_F(NativeSocketInterposerFdTest, HighFdUsesClassifierCache) { + NativeFdClassifier classifier; + ScopedFdProbeOverride override(recording_fd_probe); + g_fd_probe_calls = 0; + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_STREAM; + + EXPECT_TRUE(classifier.isStreamSocket(kFdTypeCacheSizeForTest)); + EXPECT_TRUE(classifier.isStreamSocket(kFdTypeCacheSizeForTest)); + EXPECT_EQ(1, g_fd_probe_calls.load()); + EXPECT_EQ(kFdTypeCacheSizeForTest, g_fd_probe_last_fd.load()); +} + +TEST_F(NativeSocketInterposerFdTest, HighFdTransientProbeFailureIsNotCached) { + NativeFdClassifier classifier; + ScopedFdProbeOverride override(stub_fd_probe); + int fd = kFdTypeCacheSizeForTest + 1; + g_fd_probe_calls = 0; + g_fd_probe_rc = -1; + g_fd_probe_errno = EIO; + g_fd_probe_so_type = 0; + + EXPECT_FALSE(classifier.isStreamSocket(fd)); + EXPECT_EQ(1, g_fd_probe_calls.load()); + + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_STREAM; + + EXPECT_TRUE(classifier.isStreamSocket(fd)); + EXPECT_EQ(2, g_fd_probe_calls.load()); + + EXPECT_TRUE(classifier.isStreamSocket(fd)); + EXPECT_EQ(2, g_fd_probe_calls.load()); +} + +TEST_F(NativeSocketInterposerFdTest, ClearFdTypeInvalidatesHighFdOnly) { + NativeFdClassifier classifier; + ScopedFdProbeOverride override(stub_fd_probe); + int fd = kFdTypeCacheSizeForTest + 2; + int other_fd = fd + 1; + g_fd_probe_calls = 0; + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_STREAM; + ASSERT_TRUE(classifier.isStreamSocket(fd)); + ASSERT_TRUE(classifier.isStreamSocket(other_fd)); + EXPECT_EQ(2, g_fd_probe_calls.load()); + + g_fd_probe_so_type = SOCK_DGRAM; + classifier.clearFdType(fd); + + EXPECT_FALSE(classifier.isStreamSocket(fd)); + EXPECT_TRUE(classifier.isDatagramSocket(fd)); + EXPECT_TRUE(classifier.isStreamSocket(other_fd)); + EXPECT_EQ(3, g_fd_probe_calls.load()); +} + +TEST_F(NativeSocketInterposerFdTest, ClearFdTypeCacheInvalidatesHighFds) { + NativeFdClassifier classifier; + ScopedFdProbeOverride override(stub_fd_probe); + int fd = kFdTypeCacheSizeForTest + 3; + g_fd_probe_calls = 0; + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_STREAM; + ASSERT_TRUE(classifier.isStreamSocket(fd)); + EXPECT_EQ(1, g_fd_probe_calls.load()); + + g_fd_probe_so_type = SOCK_DGRAM; + classifier.clearFdTypeCache(); + + EXPECT_FALSE(classifier.isStreamSocket(fd)); + EXPECT_TRUE(classifier.isDatagramSocket(fd)); + EXPECT_EQ(2, g_fd_probe_calls.load()); +} + +TEST_F(NativeSocketInterposerFdTest, HighFdCacheCollisionReprobesExactFd) { + NativeFdClassifier classifier; + ScopedFdProbeOverride override(recording_fd_probe); + int stream_fd = kFdTypeCacheSizeForTest + 4; + int datagram_fd = stream_fd + kHighFdCacheSizeForTest; + g_fd_probe_calls = 0; + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + + g_fd_probe_so_type = SOCK_STREAM; + ASSERT_TRUE(classifier.isStreamSocket(stream_fd)); + EXPECT_EQ(1, g_fd_probe_calls.load()); + + g_fd_probe_so_type = SOCK_DGRAM; + EXPECT_FALSE(classifier.isStreamSocket(datagram_fd)); + EXPECT_TRUE(classifier.isDatagramSocket(datagram_fd)); + EXPECT_EQ(2, g_fd_probe_calls.load()); + EXPECT_EQ(datagram_fd, g_fd_probe_last_fd.load()); + + g_fd_probe_so_type = SOCK_STREAM; + EXPECT_TRUE(classifier.isStreamSocket(stream_fd)); + EXPECT_EQ(3, g_fd_probe_calls.load()); + EXPECT_EQ(stream_fd, g_fd_probe_last_fd.load()); +} + +TEST_F(NativeSocketInterposerFdTest, ClearFdTypeInvalidatesOnlyThatFd) { + NativeFdClassifier classifier; + ScopedFdProbeOverride override(stub_fd_probe); + g_fd_probe_calls = 0; + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_STREAM; + ASSERT_TRUE(classifier.isStreamSocket(45)); + ASSERT_TRUE(classifier.isStreamSocket(46)); + EXPECT_EQ(2, g_fd_probe_calls.load()); + + g_fd_probe_so_type = SOCK_DGRAM; + classifier.clearFdType(45); + + EXPECT_FALSE(classifier.isStreamSocket(45)); + EXPECT_TRUE(classifier.isDatagramSocket(45)); + EXPECT_TRUE(classifier.isStreamSocket(46)); + EXPECT_EQ(3, g_fd_probe_calls.load()); +} + +TEST_F(NativeSocketInterposerFdTest, ClearFdTypeCacheInvalidatesCachedFds) { + NativeFdClassifier classifier; + ScopedFdProbeOverride override(stub_fd_probe); + g_fd_probe_calls = 0; + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_STREAM; + ASSERT_TRUE(classifier.isStreamSocket(47)); + EXPECT_EQ(1, g_fd_probe_calls.load()); + + g_fd_probe_so_type = SOCK_DGRAM; + classifier.clearFdTypeCache(); + + EXPECT_FALSE(classifier.isStreamSocket(47)); + EXPECT_TRUE(classifier.isDatagramSocket(47)); + EXPECT_EQ(2, g_fd_probe_calls.load()); +} + +TEST_F(NativeSocketInterposerFdTest, ClassifierInstancesHaveIndependentCacheState) { + NativeFdClassifier first; + NativeFdClassifier second; + ScopedFdProbeOverride override(stub_fd_probe); + g_fd_probe_calls = 0; + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_STREAM; + + ASSERT_TRUE(first.isStreamSocket(48)); + ASSERT_TRUE(second.isStreamSocket(48)); + EXPECT_EQ(2, g_fd_probe_calls.load()); + + first.cacheNonSocket(48); + + EXPECT_FALSE(first.isStreamSocket(48)); + EXPECT_TRUE(second.isStreamSocket(48)); + EXPECT_EQ(2, g_fd_probe_calls.load()); + + first.clearFdTypeCache(); + g_fd_probe_so_type = SOCK_DGRAM; + + EXPECT_FALSE(first.isStreamSocket(48)); + EXPECT_TRUE(second.isStreamSocket(48)); + EXPECT_EQ(3, g_fd_probe_calls.load()); +} + +TEST_F(NativeSocketInterposerFdTest, + SamplerAndInterposerClassifiersHaveIndependentCacheState) { + NativeSocketInterposer* interposer = NativeSocketInterposer::instance(); + NativeSocketSampler* sampler = NativeSocketSampler::instance(); + interposer->clearFdTypeCache(); + sampler->clearFdCache(); + ScopedFdProbeOverride override(stub_fd_probe); + g_fd_probe_calls = 0; + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + g_fd_probe_so_type = SOCK_STREAM; + const int fd = 49; + + ASSERT_TRUE(interposer->isStreamSocket(fd)); + EXPECT_EQ(1, g_fd_probe_calls.load()); + ASSERT_TRUE(sampler->isSocketForTest(fd)); + EXPECT_EQ(2, g_fd_probe_calls.load()); + + g_fd_probe_so_type = SOCK_DGRAM; + interposer->clearFdType(fd); + + EXPECT_FALSE(interposer->isStreamSocket(fd)); + EXPECT_TRUE(interposer->isDatagramSocket(fd)); + EXPECT_EQ(3, g_fd_probe_calls.load()); + EXPECT_TRUE(sampler->isSocketForTest(fd)); + EXPECT_EQ(3, g_fd_probe_calls.load()); + + sampler->clearFdCacheEntry(fd); + + EXPECT_FALSE(sampler->isSocketForTest(fd)); + EXPECT_EQ(4, g_fd_probe_calls.load()); + interposer->clearFdTypeCache(); + sampler->clearFdCache(); +} + +TEST_F(NativeSocketInterposerFdTest, ConcurrentClassifierReadsAndClearsAreSafe) { + NativeFdClassifier classifier; + ScopedFdProbeOverride override(stub_fd_probe); + g_fd_probe_calls = 0; + g_fd_probe_rc = 0; + g_fd_probe_errno = 0; + static constexpr int kReaders = 4; + std::atomic start{false}; + std::atomic stop{false}; + std::atomic ready_readers{0}; + std::atomic reads{0}; + int fd = 48; + + std::thread clearer([&]() { + while (ready_readers.load(std::memory_order_acquire) < kReaders) { + std::this_thread::yield(); + } + start.store(true, std::memory_order_release); + for (int i = 0; i < 1000; i++) { + g_fd_probe_so_type = (i % 2 == 0) ? SOCK_STREAM : SOCK_DGRAM; + classifier.clearFdType(fd); + if ((i % 16) == 0) { + classifier.clearFdTypeCache(); + } + std::this_thread::yield(); + } + stop.store(true, std::memory_order_release); + }); + + std::vector readers; + for (int i = 0; i < kReaders; i++) { + readers.emplace_back([&]() { + ready_readers.fetch_add(1, std::memory_order_release); + while (!start.load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + while (!stop.load(std::memory_order_acquire)) { + (void)classifier.isStreamSocket(fd); + (void)classifier.isDatagramSocket(fd); + reads.fetch_add(1, std::memory_order_relaxed); + } + }); + } + + clearer.join(); + for (auto& reader : readers) { + reader.join(); + } + + EXPECT_GT(reads.load(std::memory_order_relaxed), 0); + EXPECT_GT(g_fd_probe_calls.load(), 0); +} + +TEST_F(NativeSocketInterposerFdTest, CloseHookInvalidatesFdBeforeReuse) { + int stream_fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds)); + + int reused_fd = stream_fds[0]; + EXPECT_TRUE(NativeSocketInterposer::instance()->isStreamSocket(reused_fd)); + ASSERT_EQ(0, closeThroughHook(reused_fd)); + + int datagram_fd = datagramSocketAtFd(reused_fd); + ASSERT_EQ(reused_fd, datagram_fd); + EXPECT_FALSE(NativeSocketInterposer::instance()->isStreamSocket(datagram_fd)); + + ASSERT_EQ(0, closeThroughHook(datagram_fd)); + ASSERT_EQ(0, closeThroughHook(stream_fds[1])); +} + +TEST_F(NativeSocketInterposerFdTest, + CloseHookInvalidatesNativeSocketSamplerFdStateBeforeReuse) { + int stream_fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds)); + + NativeSocketSampler* sampler = NativeSocketSampler::instance(); + int reused_fd = stream_fds[0]; + EXPECT_TRUE(sampler->isSocketForTest(reused_fd)); + sampler->fdAddrCacheInsertForTest(reused_fd, "127.0.0.1:12345"); + ASSERT_TRUE(sampler->fdAddrCacheContainsForTest(reused_fd)); + + errno = E2BIG; + ASSERT_EQ(0, closeThroughHook(reused_fd)); + EXPECT_EQ(E2BIG, errno); + EXPECT_FALSE(sampler->fdAddrCacheContainsForTest(reused_fd)); + + int datagram_fd = datagramSocketAtFd(reused_fd); + ASSERT_EQ(reused_fd, datagram_fd); + EXPECT_FALSE(sampler->isSocketForTest(datagram_fd)); + + ASSERT_EQ(0, closeThroughHook(datagram_fd)); + ASSERT_EQ(0, closeThroughHook(stream_fds[1])); +} + +TEST_F(NativeSocketInterposerFdTest, FailedCloseInvalidatesCachesAndPreservesErrno) { + int stream_fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds)); + + int fd = stream_fds[0]; + NativeSocketSampler* sampler = NativeSocketSampler::instance(); + EXPECT_TRUE(NativeSocketInterposer::instance()->isStreamSocket(fd)); + EXPECT_TRUE(sampler->isSocketForTest(fd)); + sampler->fdAddrCacheInsertForTest(fd, "127.0.0.1:12345"); + ASSERT_TRUE(sampler->fdAddrCacheContainsForTest(fd)); + + setOriginalFunction(NativeSocketInterposer::HOOK_CLOSE, + reinterpret_cast(stub_close)); + g_close_ret = -1; + g_close_errno = EINTR; + uint64_t probes_before = NativeFdClassifier::probeCountForTest(); + + errno = E2BIG; + EXPECT_EQ(-1, closeThroughHook(fd)); + EXPECT_EQ(EINTR, errno); + EXPECT_FALSE(sampler->fdAddrCacheContainsForTest(fd)); + EXPECT_TRUE(NativeSocketInterposer::instance()->isStreamSocket(fd)); + EXPECT_GT(NativeFdClassifier::probeCountForTest(), probes_before); + + setOriginalFunction(NativeSocketInterposer::HOOK_CLOSE, + reinterpret_cast(::close)); + ASSERT_EQ(0, closeThroughHook(fd)); + ASSERT_EQ(0, closeThroughHook(stream_fds[1])); +} + +TEST_F(NativeSocketInterposerFdTest, RepeatedCacheClearsDoNotResurrectOldFdType) { + int stream_fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds)); + + int reused_fd = stream_fds[0]; + EXPECT_TRUE(NativeSocketInterposer::instance()->isStreamSocket(reused_fd)); + ASSERT_EQ(0, close(reused_fd)); + + for (int i = 0; i < 1024; i++) { + NativeSocketInterposer::instance()->clearFdTypeCache(); + } + + int datagram_fd = datagramSocketAtFd(reused_fd); + ASSERT_EQ(reused_fd, datagram_fd); + EXPECT_FALSE(NativeSocketInterposer::instance()->isStreamSocket(datagram_fd)); + + ASSERT_EQ(0, closeThroughHook(datagram_fd)); + ASSERT_EQ(0, closeThroughHook(stream_fds[1])); +} + +TEST_F(NativeSocketInterposerFdTest, Dup2InvalidatesTargetFdBeforeReuse) { + int stream_fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds)); + int pipe_fds[2]; + ASSERT_EQ(0, pipe(pipe_fds)); + + int target_fd = stream_fds[0]; + EXPECT_TRUE(NativeSocketInterposer::instance()->isStreamSocket(target_fd)); + + errno = E2BIG; + ASSERT_EQ(target_fd, dup2ThroughHook(pipe_fds[0], target_fd)); + EXPECT_EQ(E2BIG, errno); + EXPECT_FALSE(NativeSocketInterposer::instance()->isStreamSocket(target_fd)); + + ASSERT_EQ(0, closeThroughHook(target_fd)); + ASSERT_EQ(0, closeThroughHook(stream_fds[1])); + ASSERT_EQ(0, closeThroughHook(pipe_fds[0])); + ASSERT_EQ(0, closeThroughHook(pipe_fds[1])); +} + +TEST_F(NativeSocketInterposerFdTest, + Dup2InvalidatesNativeSocketSamplerTargetFdStateBeforeReuse) { + int stream_fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds)); + int pipe_fds[2]; + ASSERT_EQ(0, pipe(pipe_fds)); + + NativeSocketSampler* sampler = NativeSocketSampler::instance(); + int target_fd = stream_fds[0]; + EXPECT_TRUE(sampler->isSocketForTest(target_fd)); + sampler->fdAddrCacheInsertForTest(target_fd, "127.0.0.1:12345"); + ASSERT_TRUE(sampler->fdAddrCacheContainsForTest(target_fd)); + + errno = E2BIG; + ASSERT_EQ(target_fd, dup2ThroughHook(pipe_fds[0], target_fd)); + EXPECT_EQ(E2BIG, errno); + EXPECT_FALSE(sampler->fdAddrCacheContainsForTest(target_fd)); + EXPECT_FALSE(sampler->isSocketForTest(target_fd)); + + ASSERT_EQ(0, closeThroughHook(target_fd)); + ASSERT_EQ(0, closeThroughHook(stream_fds[1])); + ASSERT_EQ(0, closeThroughHook(pipe_fds[0])); + ASSERT_EQ(0, closeThroughHook(pipe_fds[1])); +} + +TEST_F(NativeSocketInterposerFdTest, FailedDup2DoesNotInvalidateTargetFd) { + int stream_fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds)); + + int target_fd = stream_fds[0]; + EXPECT_TRUE(NativeSocketInterposer::instance()->isStreamSocket(target_fd)); + + errno = 0; + EXPECT_EQ(-1, dup2ThroughHook(-1, target_fd)); + EXPECT_EQ(EBADF, errno); + EXPECT_TRUE(NativeSocketInterposer::instance()->isStreamSocket(target_fd)); + + ASSERT_EQ(0, closeThroughHook(target_fd)); + ASSERT_EQ(0, closeThroughHook(stream_fds[1])); +} + +TEST_F(NativeSocketInterposerFdTest, + FailedDup2DoesNotInvalidateNativeSocketSamplerTargetFdState) { + int stream_fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds)); + + NativeSocketSampler* sampler = NativeSocketSampler::instance(); + int target_fd = stream_fds[0]; + EXPECT_TRUE(sampler->isSocketForTest(target_fd)); + sampler->fdAddrCacheInsertForTest(target_fd, "127.0.0.1:12345"); + ASSERT_TRUE(sampler->fdAddrCacheContainsForTest(target_fd)); + + errno = 0; + EXPECT_EQ(-1, dup2ThroughHook(-1, target_fd)); + EXPECT_EQ(EBADF, errno); + EXPECT_TRUE(sampler->isSocketForTest(target_fd)); + EXPECT_TRUE(sampler->fdAddrCacheContainsForTest(target_fd)); + + ASSERT_EQ(0, closeThroughHook(target_fd)); + ASSERT_EQ(0, closeThroughHook(stream_fds[1])); +} + +#ifdef SYS_dup3 +TEST_F(NativeSocketInterposerFdTest, Dup3InvalidatesTargetFdBeforeReuse) { + int stream_fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds)); + int pipe_fds[2]; + ASSERT_EQ(0, pipe(pipe_fds)); + + int target_fd = stream_fds[0]; + EXPECT_TRUE(NativeSocketInterposer::instance()->isStreamSocket(target_fd)); + + errno = E2BIG; + ASSERT_EQ(target_fd, dup3ThroughHook(pipe_fds[0], target_fd, 0)); + EXPECT_EQ(E2BIG, errno); + EXPECT_FALSE(NativeSocketInterposer::instance()->isStreamSocket(target_fd)); + + ASSERT_EQ(0, closeThroughHook(target_fd)); + ASSERT_EQ(0, closeThroughHook(stream_fds[1])); + ASSERT_EQ(0, closeThroughHook(pipe_fds[0])); + ASSERT_EQ(0, closeThroughHook(pipe_fds[1])); +} + +TEST_F(NativeSocketInterposerFdTest, Dup3PreservesErrnoOnSuccessfulInvalidation) { + int stream_fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds)); + int pipe_fds[2]; + ASSERT_EQ(0, pipe(pipe_fds)); + + int target_fd = stream_fds[0]; + EXPECT_TRUE(NativeSocketInterposer::instance()->isStreamSocket(target_fd)); + + errno = E2BIG; + ASSERT_EQ(target_fd, dup3ThroughHook(pipe_fds[0], target_fd, O_CLOEXEC)); + EXPECT_EQ(E2BIG, errno); + EXPECT_FALSE(NativeSocketInterposer::instance()->isStreamSocket(target_fd)); + + ASSERT_EQ(0, closeThroughHook(target_fd)); + ASSERT_EQ(0, closeThroughHook(stream_fds[1])); + ASSERT_EQ(0, closeThroughHook(pipe_fds[0])); + ASSERT_EQ(0, closeThroughHook(pipe_fds[1])); +} + +TEST_F(NativeSocketInterposerFdTest, + Dup3InvalidatesNativeSocketSamplerTargetFdStateBeforeReuse) { + int stream_fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds)); + int pipe_fds[2]; + ASSERT_EQ(0, pipe(pipe_fds)); + + NativeSocketSampler* sampler = NativeSocketSampler::instance(); + int target_fd = stream_fds[0]; + EXPECT_TRUE(sampler->isSocketForTest(target_fd)); + sampler->fdAddrCacheInsertForTest(target_fd, "127.0.0.1:12345"); + ASSERT_TRUE(sampler->fdAddrCacheContainsForTest(target_fd)); + + errno = E2BIG; + ASSERT_EQ(target_fd, dup3ThroughHook(pipe_fds[0], target_fd, 0)); + EXPECT_EQ(E2BIG, errno); + EXPECT_FALSE(sampler->fdAddrCacheContainsForTest(target_fd)); + EXPECT_FALSE(sampler->isSocketForTest(target_fd)); + + ASSERT_EQ(0, closeThroughHook(target_fd)); + ASSERT_EQ(0, closeThroughHook(stream_fds[1])); + ASSERT_EQ(0, closeThroughHook(pipe_fds[0])); + ASSERT_EQ(0, closeThroughHook(pipe_fds[1])); +} +#endif + +TEST_F(NativeSocketInterposerFdTest, ConcurrentFdReuseInvalidationDoesNotPreserveStaleStreamType) { + for (int i = 0; i < 64; i++) { + int stream_fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds)); + int reused_fd = stream_fds[0]; + ASSERT_TRUE(NativeSocketInterposer::instance()->isStreamSocket(reused_fd)); + + std::atomic done{false}; + std::thread reader([&]() { + while (!done.load(std::memory_order_acquire)) { + (void)NativeSocketInterposer::instance()->isStreamSocket(reused_fd); + std::this_thread::yield(); + } + }); + + ASSERT_EQ(0, closeThroughHook(reused_fd)); + done.store(true, std::memory_order_release); + reader.join(); + + int datagram_fd = datagramSocketAtFd(reused_fd); + ASSERT_EQ(reused_fd, datagram_fd); + + EXPECT_FALSE(NativeSocketInterposer::instance()->isStreamSocket(datagram_fd)); + EXPECT_TRUE(NativeSocketInterposer::instance()->isDatagramSocket(datagram_fd)); + ASSERT_EQ(0, closeThroughHook(datagram_fd)); + ASSERT_EQ(0, closeThroughHook(stream_fds[1])); + } +} + +TEST_F(NativeSocketInterposerFdTest, + ProbeStraddlingCloseAndReuseCannotPublishStaleStreamType) { + int stream_fds[2]; + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds)); + int pipe_fds[2]; + ASSERT_EQ(0, pipe(pipe_fds)); + int reused_fd = stream_fds[0]; + + g_blocking_probe_started.store(false, std::memory_order_release); + g_release_blocking_probe.store(false, std::memory_order_release); + std::atomic stale_stream{true}; + { + ScopedFdProbeOverride override(blocking_stream_fd_probe); + std::thread probe([&]() { + stale_stream.store( + NativeSocketInterposer::instance()->isStreamSocket(reused_fd), + std::memory_order_release); + }); + + while (!g_blocking_probe_started.load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + int close_result = closeThroughHook(reused_fd); + int dup_result = dup2ThroughHook(pipe_fds[0], reused_fd); + g_release_blocking_probe.store(true, std::memory_order_release); + probe.join(); + ASSERT_EQ(0, close_result); + ASSERT_EQ(reused_fd, dup_result); + } + + EXPECT_FALSE(stale_stream.load(std::memory_order_acquire)); + EXPECT_FALSE(NativeSocketInterposer::instance()->isStreamSocket(reused_fd)); + + ASSERT_EQ(0, closeThroughHook(reused_fd)); + ASSERT_EQ(0, closeThroughHook(stream_fds[1])); + ASSERT_EQ(0, closeThroughHook(pipe_fds[0])); + ASSERT_EQ(0, closeThroughHook(pipe_fds[1])); +} + +TEST_F(NativeSocketInterposerFdTest, RejectsNonSockets) { + int fds[2]; + ASSERT_EQ(0, pipe(fds)); + EXPECT_FALSE(NativeSocketInterposer::instance()->isStreamSocket(fds[0])); + ASSERT_EQ(0, closeThroughHook(fds[0])); + ASSERT_EQ(0, closeThroughHook(fds[1])); +} + +#endif // __linux__ diff --git a/ddprof-lib/src/test/cpp/nativeSocketSampler_ut.cpp b/ddprof-lib/src/test/cpp/nativeSocketSampler_ut.cpp index ee81af7e51..a7a3b0327e 100644 --- a/ddprof-lib/src/test/cpp/nativeSocketSampler_ut.cpp +++ b/ddprof-lib/src/test/cpp/nativeSocketSampler_ut.cpp @@ -22,7 +22,9 @@ #include "libraryPatcher.h" #include +#include #include +#include // --------------------------------------------------------------------------- // Stub tracking @@ -57,6 +59,76 @@ static ssize_t stub_read(int /*fd*/, void* /*buf*/, size_t /*len*/) { return g_read_ret.load(); } +static const int kSamplerFdTypeCacheSizeForTest = 65536; +static const int kSamplerHighFdCacheSizeForTest = 4096; +static std::atomic g_probe_calls{0}; +static std::atomic g_probe_last_fd{-1}; +static std::atomic g_probe_rc{0}; +static std::atomic g_probe_errno{0}; +static std::atomic g_probe_so_type{SOCK_STREAM}; + +static int stub_probe(int fd, int *so_type, int *probe_errno) { + g_probe_calls++; + g_probe_last_fd = fd; + *so_type = g_probe_so_type.load(); + *probe_errno = g_probe_errno.load(); + return g_probe_rc.load(); +} + +static int datagramSocketAtFdForTest(int target_fd) { + int datagram_fd = socket(AF_INET, SOCK_DGRAM, 0); + if (datagram_fd < 0) { + return -1; + } + if (datagram_fd == target_fd) { + return datagram_fd; + } + int dup_fd = dup2(datagram_fd, target_fd); + int saved_errno = errno; + close(datagram_fd); + errno = saved_errno; + return dup_fd; +} + +class ScopedFdForTest { +public: + explicit ScopedFdForTest(int fd = -1) : _fd(fd) {} + ~ScopedFdForTest() { + if (_fd >= 0) { + close(_fd); + } + } + ScopedFdForTest(const ScopedFdForTest&) = delete; + ScopedFdForTest& operator=(const ScopedFdForTest&) = delete; + + void reset(int fd) { + if (_fd >= 0) { + close(_fd); + } + _fd = fd; + } + + int release() { + int fd = _fd; + _fd = -1; + return fd; + } + +private: + int _fd; +}; + +class ScopedSamplerProbeOverride { +public: + explicit ScopedSamplerProbeOverride(NativeSocketSampler::ProbeOverride probe) { + NativeSocketSampler::setProbeOverrideForTest(probe); + } + + ~ScopedSamplerProbeOverride() { + NativeSocketSampler::setProbeOverrideForTest(nullptr); + } +}; + // --------------------------------------------------------------------------- // Test fixture — installs stubs as the "original" function pointers so the // hooks invoke them without needing GOT patching or a running JVM. @@ -345,6 +417,231 @@ TEST(NativeSocketSamplerLruTest, ClearResetsCache) { << "clearFdCache() must empty both the map and the LRU list"; } +TEST(NativeSocketSamplerLruTest, ClearFdCacheEntryRemovesOnlyRequestedEntry) { + NativeSocketSampler* inst = NativeSocketSampler::instance(); + inst->clearFdCache(); + + inst->fdAddrCacheInsertForTest(1, "1.2.3.4:100"); + inst->fdAddrCacheInsertForTest(2, "1.2.3.4:200"); + ASSERT_EQ(inst->fdAddrCacheSizeForTest(), 2); + + inst->clearFdCacheEntry(1); + + EXPECT_FALSE(inst->fdAddrCacheContainsForTest(1)); + EXPECT_TRUE(inst->fdAddrCacheContainsForTest(2)); + EXPECT_EQ(inst->fdAddrCacheSizeForTest(), 1); + inst->clearFdCache(); +} + +TEST(NativeSocketSamplerLruTest, ClearFdCacheEntryHandlesInvalidFds) { + NativeSocketSampler* inst = NativeSocketSampler::instance(); + inst->clearFdCache(); + + inst->fdAddrCacheInsertForTest(7, "1.2.3.4:700"); + + inst->clearFdCacheEntry(-1); + inst->clearFdCacheEntry(NativeSocketSampler::MAX_FD_CACHE + 1); + + EXPECT_TRUE(inst->fdAddrCacheContainsForTest(7)); + EXPECT_EQ(inst->fdAddrCacheSizeForTest(), 1); + inst->clearFdCache(); +} + +TEST(NativeSocketSamplerLruTest, ClearFdCacheEntryInvalidatesFdTypeBeforeReuse) { + NativeSocketSampler* inst = NativeSocketSampler::instance(); + inst->clearFdCache(); + + int stream_fds[2]; + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds), 0); + + int reused_fd = stream_fds[0]; + EXPECT_TRUE(inst->isSocketForTest(reused_fd)); + close(reused_fd); + + inst->clearFdCacheEntry(reused_fd); + + int datagram_fd = socket(AF_INET, SOCK_DGRAM, 0); + ASSERT_EQ(datagram_fd, reused_fd); + EXPECT_FALSE(inst->isSocketForTest(datagram_fd)); + + close(datagram_fd); + close(stream_fds[1]); + inst->clearFdCache(); +} + +TEST(NativeSocketSamplerFdTypeTest, RevalidateSocketDowngradesReusedFdToNonSocket) { + NativeSocketSampler* inst = NativeSocketSampler::instance(); + inst->clearFdCache(); + + int stream_fds[2]; + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, stream_fds), 0); + + int reused_fd = stream_fds[0]; + ScopedFdForTest stream_peer(stream_fds[1]); + ASSERT_TRUE(inst->isSocketForTest(reused_fd)); + close(reused_fd); + + int datagram_fd = datagramSocketAtFdForTest(reused_fd); + ScopedFdForTest datagram(datagram_fd); + ASSERT_EQ(datagram_fd, reused_fd); + + EXPECT_FALSE(inst->revalidateSocketForTest(datagram_fd)); + EXPECT_FALSE(inst->isSocketForTest(datagram_fd)); + + inst->clearFdCache(); +} + +TEST(NativeSocketSamplerFdTypeTest, HighFdStreamVerdictIsCached) { + NativeSocketSampler* inst = NativeSocketSampler::instance(); + inst->clearFdCache(); + ScopedSamplerProbeOverride override(stub_probe); + g_probe_calls = 0; + g_probe_rc = 0; + g_probe_errno = 0; + g_probe_so_type = SOCK_STREAM; + int fd = kSamplerFdTypeCacheSizeForTest; + + EXPECT_TRUE(inst->isSocketForTest(fd)); + EXPECT_TRUE(inst->isSocketForTest(fd)); + + EXPECT_EQ(g_probe_calls.load(), 1); + EXPECT_EQ(g_probe_last_fd.load(), fd); + inst->clearFdCache(); +} + +TEST(NativeSocketSamplerFdTypeTest, HighFdEnotsockVerdictIsCached) { + NativeSocketSampler* inst = NativeSocketSampler::instance(); + inst->clearFdCache(); + ScopedSamplerProbeOverride override(stub_probe); + g_probe_calls = 0; + g_probe_rc = -1; + g_probe_errno = ENOTSOCK; + g_probe_so_type = 0; + int fd = kSamplerFdTypeCacheSizeForTest + 1; + + EXPECT_FALSE(inst->isSocketForTest(fd)); + EXPECT_FALSE(inst->isSocketForTest(fd)); + + EXPECT_EQ(g_probe_calls.load(), 1); + inst->clearFdCache(); +} + +TEST(NativeSocketSamplerFdTypeTest, HighFdTransientProbeFailureIsNotCached) { + NativeSocketSampler* inst = NativeSocketSampler::instance(); + inst->clearFdCache(); + ScopedSamplerProbeOverride override(stub_probe); + g_probe_calls = 0; + g_probe_rc = -1; + g_probe_errno = EBADF; + g_probe_so_type = 0; + int fd = kSamplerFdTypeCacheSizeForTest + 2; + + EXPECT_FALSE(inst->isSocketForTest(fd)); + EXPECT_EQ(g_probe_calls.load(), 1); + + g_probe_rc = 0; + g_probe_errno = 0; + g_probe_so_type = SOCK_STREAM; + + EXPECT_TRUE(inst->isSocketForTest(fd)); + EXPECT_TRUE(inst->isSocketForTest(fd)); + EXPECT_EQ(g_probe_calls.load(), 2); + inst->clearFdCache(); +} + +TEST(NativeSocketSamplerFdTypeTest, ClearFdCacheEntryInvalidatesHighFdOnly) { + NativeSocketSampler* inst = NativeSocketSampler::instance(); + inst->clearFdCache(); + ScopedSamplerProbeOverride override(stub_probe); + g_probe_calls = 0; + g_probe_rc = 0; + g_probe_errno = 0; + g_probe_so_type = SOCK_STREAM; + int fd = kSamplerFdTypeCacheSizeForTest + 3; + int other_fd = fd + 1; + + ASSERT_TRUE(inst->isSocketForTest(fd)); + ASSERT_TRUE(inst->isSocketForTest(other_fd)); + EXPECT_EQ(g_probe_calls.load(), 2); + + g_probe_so_type = SOCK_DGRAM; + inst->clearFdCacheEntry(fd); + + EXPECT_FALSE(inst->isSocketForTest(fd)); + EXPECT_TRUE(inst->isSocketForTest(other_fd)); + EXPECT_EQ(g_probe_calls.load(), 3); + inst->clearFdCache(); +} + +TEST(NativeSocketSamplerFdTypeTest, ClearFdCacheInvalidatesHighFdsByGeneration) { + NativeSocketSampler* inst = NativeSocketSampler::instance(); + inst->clearFdCache(); + ScopedSamplerProbeOverride override(stub_probe); + g_probe_calls = 0; + g_probe_rc = 0; + g_probe_errno = 0; + g_probe_so_type = SOCK_STREAM; + int fd = kSamplerFdTypeCacheSizeForTest + 4; + + ASSERT_TRUE(inst->isSocketForTest(fd)); + EXPECT_EQ(g_probe_calls.load(), 1); + + g_probe_so_type = SOCK_DGRAM; + inst->clearFdCache(); + + EXPECT_FALSE(inst->isSocketForTest(fd)); + EXPECT_EQ(g_probe_calls.load(), 2); + inst->clearFdCache(); +} + +TEST(NativeSocketSamplerFdTypeTest, HighFdCacheCollisionReprobesExactFd) { + NativeSocketSampler* inst = NativeSocketSampler::instance(); + inst->clearFdCache(); + ScopedSamplerProbeOverride override(stub_probe); + g_probe_calls = 0; + g_probe_rc = 0; + g_probe_errno = 0; + int stream_fd = kSamplerFdTypeCacheSizeForTest + 5; + int datagram_fd = stream_fd + kSamplerHighFdCacheSizeForTest; + + g_probe_so_type = SOCK_STREAM; + ASSERT_TRUE(inst->isSocketForTest(stream_fd)); + EXPECT_EQ(g_probe_calls.load(), 1); + + g_probe_so_type = SOCK_DGRAM; + EXPECT_FALSE(inst->isSocketForTest(datagram_fd)); + EXPECT_EQ(g_probe_calls.load(), 2); + EXPECT_EQ(g_probe_last_fd.load(), datagram_fd); + + g_probe_so_type = SOCK_STREAM; + EXPECT_TRUE(inst->isSocketForTest(stream_fd)); + EXPECT_EQ(g_probe_calls.load(), 3); + EXPECT_EQ(g_probe_last_fd.load(), stream_fd); + inst->clearFdCache(); +} + +TEST_F(NativeSocketSamplerHookTest, HighFdWriteHookReusesCachedSocketVerdict) { + NativeSocketSampler* inst = NativeSocketSampler::instance(); + inst->clearFdCache(); + ScopedSamplerProbeOverride override(stub_probe); + g_probe_calls = 0; + g_probe_rc = 0; + g_probe_errno = 0; + g_probe_so_type = SOCK_STREAM; + g_write_ret = -1; + int fd = kSamplerFdTypeCacheSizeForTest + 6; + char buf[16] = {}; + + bool prev = LibraryPatcher::_socket_active.exchange(true, std::memory_order_release); + EXPECT_EQ(-1, NativeSocketSampler::write_hook(fd, buf, sizeof(buf))); + EXPECT_EQ(-1, NativeSocketSampler::write_hook(fd, buf, sizeof(buf))); + LibraryPatcher::_socket_active.store(prev, std::memory_order_release); + + EXPECT_EQ(g_write_calls.load(), 2); + EXPECT_EQ(g_probe_calls.load(), 1); + inst->clearFdCache(); +} + TEST(NativeSocketSamplerLruTest, InsertAndLookupPreservesEntries) { NativeSocketSampler* inst = NativeSocketSampler::instance(); inst->clearFdCache(); diff --git a/ddprof-lib/src/test/cpp/threadFilter_ut.cpp b/ddprof-lib/src/test/cpp/threadFilter_ut.cpp index 0ed6c86804..92708c0901 100644 --- a/ddprof-lib/src/test/cpp/threadFilter_ut.cpp +++ b/ddprof-lib/src/test/cpp/threadFilter_ut.cpp @@ -708,6 +708,105 @@ TEST_F(ThreadFilterTest, ContextScopeNeverSuppressesOwnedBlock) { EXPECT_FALSE(filter->isOwnedBlockSuppressionCandidate(entry)); } +TEST_F(ThreadFilterTest, OwnedNativeIoSuppressesBeforeAnyWallSample) { + filter->init(nullptr, true); + int slot_id = filter->registerThread(1234); + ASSERT_GE(slot_id, 0); + ThreadFilter::Slot* slot = filter->slotForId(slot_id); + ASSERT_NE(nullptr, slot); + u64 token = filter->enterBlockedRun( + slot_id, OSThreadState::IO_WAIT, BlockRunOwner::NATIVE); + ASSERT_NE(0ULL, token); + + ThreadEntry entry{1234, slot, slot->lifecycleGeneration(), + slot->recordingEpoch()}; + u64 generation = ThreadFilter::tokenGeneration(token); + EXPECT_EQ(0u, slot->sampledBlockGeneration()); + EXPECT_TRUE(filter->isOwnedBlockSuppressionCandidate(entry)); + EXPECT_FALSE(filter->isOwnedBlockSuppressionCandidate( + {1235, slot, slot->lifecycleGeneration(), slot->recordingEpoch()})); + EXPECT_FALSE(filter->isOwnedBlockSuppressionCandidate( + {1234, slot, slot->lifecycleGeneration() + 1, + slot->recordingEpoch()})); + + ASSERT_TRUE(filter->exitBlockedRun(slot_id, generation)); + EXPECT_FALSE(filter->isOwnedBlockSuppressionCandidate(entry)); +} + +TEST_F(ThreadFilterTest, ConcurrentNativeExitInvalidatesSuppressionSnapshot) { + filter->init(nullptr, true); + int slot_id = filter->registerThread(1234); + ASSERT_GE(slot_id, 0); + ThreadFilter::Slot* slot = filter->slotForId(slot_id); + ASSERT_NE(nullptr, slot); + u64 token = filter->enterBlockedRun( + slot_id, OSThreadState::IO_WAIT, BlockRunOwner::NATIVE); + ASSERT_NE(0ULL, token); + + ThreadEntry entry{1234, slot, slot->lifecycleGeneration(), + slot->recordingEpoch()}; + struct SnapshotPause { + std::atomic reached{false}; + std::atomic resume{false}; + } pause; + filter->setSuppressionSnapshotHookForTest( + [](void* raw) { + SnapshotPause* pause = static_cast(raw); + pause->reached.store(true, std::memory_order_release); + while (!pause->resume.load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + }, + &pause); + + std::atomic suppressed{true}; + std::thread reader([&] { + suppressed.store(filter->isOwnedBlockSuppressionCandidate(entry), + std::memory_order_release); + }); + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!pause.reached.load(std::memory_order_acquire) && + std::chrono::steady_clock::now() < deadline) { + std::this_thread::yield(); + } + if (!pause.reached.load(std::memory_order_acquire)) { + pause.resume.store(true, std::memory_order_release); + reader.join(); + filter->setSuppressionSnapshotHookForTest(nullptr, nullptr); + GTEST_FAIL() << "Suppression reader did not reach the snapshot barrier"; + } + + EXPECT_TRUE(filter->exitBlockedRun( + slot_id, ThreadFilter::tokenGeneration(token))); + pause.resume.store(true, std::memory_order_release); + reader.join(); + filter->setSuppressionSnapshotHookForTest(nullptr, nullptr); + + EXPECT_FALSE(suppressed.load(std::memory_order_acquire)); +} + +TEST_F(ThreadFilterTest, OwnedJvmtiBlockSuppressesOnlyAfterSuccessfulWallSample) { + filter->init(nullptr, true); + int slot_id = filter->registerThread(1234); + ASSERT_GE(slot_id, 0); + ThreadFilter::Slot* slot = filter->slotForId(slot_id); + ASSERT_NE(nullptr, slot); + u64 token = filter->enterBlockedRun( + slot_id, OSThreadState::MONITOR_WAIT, BlockRunOwner::JVMTI); + ASSERT_NE(0ULL, token); + + ThreadEntry entry{1234, slot, slot->lifecycleGeneration(), + slot->recordingEpoch()}; + u64 generation = ThreadFilter::tokenGeneration(token); + EXPECT_FALSE(filter->isOwnedBlockSuppressionCandidate(entry)); + + slot->markBlockGenerationSampled(generation); + EXPECT_TRUE(filter->isOwnedBlockSuppressionCandidate(entry)); + + ASSERT_TRUE(filter->exitBlockedRun(slot_id, generation)); + EXPECT_FALSE(filter->isOwnedBlockSuppressionCandidate(entry)); +} + TEST_F(ThreadFilterTest, ContextEpochDisablesOwnedBlockSuppression) { filter->init(nullptr, true); int slot_id = filter->registerThread(1234); diff --git a/ddprof-lib/src/test/resources/native-libs/reladyn-lib/reladyn.c b/ddprof-lib/src/test/resources/native-libs/reladyn-lib/reladyn.c index 4d87f57edf..01e7691d70 100644 --- a/ddprof-lib/src/test/resources/native-libs/reladyn-lib/reladyn.c +++ b/ddprof-lib/src/test/resources/native-libs/reladyn-lib/reladyn.c @@ -1,13 +1,18 @@ /* * Copyright The async-profiler authors + * Copyright 2026, Datadog, Inc. * SPDX-License-Identifier: Apache-2.0 */ #include #include +#include // Force pthread_setspecific into .rela.dyn with R_X86_64_GLOB_DAT. int (*indirect_pthread_setspecific)(pthread_key_t, const void*); // Force pthread_exit into .rela.dyn with R_X86_64_64. void (*static_pthread_exit)(void*) = pthread_exit; +// Force read into .rela.plt (direct call) and two distinct .rela.dyn slots. +ssize_t (*static_read)(int, void*, size_t) = read; +ssize_t (*static_read_second)(int, void*, size_t) = read; void* thread_function(void* arg) { printf("Thread running\n"); return NULL; @@ -25,4 +30,16 @@ int reladyn() { // Use pthread_exit via the static pointer, forces into .rela.dyn as R_X86_64_64. static_pthread_exit(NULL); return 0; -} \ No newline at end of file +} + +ssize_t reladyn_direct_read(int fd, void* buffer, size_t size) { + return read(fd, buffer, size); +} + +ssize_t reladyn_indirect_read(int fd, void* buffer, size_t size) { + return static_read(fd, buffer, size); +} + +ssize_t reladyn_second_indirect_read(int fd, void* buffer, size_t size) { + return static_read_second(fd, buffer, size); +} diff --git a/ddprof-lib/src/test/resources/native-libs/unloadable-io-lib/Makefile b/ddprof-lib/src/test/resources/native-libs/unloadable-io-lib/Makefile new file mode 100644 index 0000000000..fca9ddac41 --- /dev/null +++ b/ddprof-lib/src/test/resources/native-libs/unloadable-io-lib/Makefile @@ -0,0 +1,6 @@ +# Copyright 2026, Datadog, Inc. +# SPDX-License-Identifier: Apache-2.0 + +TARGET_DIR = ../build/test/resources/native-libs/unloadable-io-lib +all: + gcc -fPIC -shared -o $(TARGET_DIR)/libunloadable-io.so unloadable_io.c diff --git a/ddprof-lib/src/test/resources/native-libs/unloadable-io-lib/unloadable_io.c b/ddprof-lib/src/test/resources/native-libs/unloadable-io-lib/unloadable_io.c new file mode 100644 index 0000000000..f73e5d00c8 --- /dev/null +++ b/ddprof-lib/src/test/resources/native-libs/unloadable-io-lib/unloadable_io.c @@ -0,0 +1,12 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +ssize_t unloadable_read(int fd, void* buffer, size_t size) { + return read(fd, buffer, size); +} diff --git a/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/throughput/NativeSocketIoBenchmark.java b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/throughput/NativeSocketIoBenchmark.java new file mode 100644 index 0000000000..c6ebc61d6a --- /dev/null +++ b/ddprof-stresstest/src/jmh/java/com/datadoghq/profiler/stresstest/scenarios/throughput/NativeSocketIoBenchmark.java @@ -0,0 +1,318 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.datadoghq.profiler.stresstest.scenarios.throughput; + +import com.datadoghq.profiler.JavaProfiler; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.Pipe; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +@State(Scope.Benchmark) +/** Compares representative I/O costs with native TaskBlock interposition disabled and enabled. */ +public class NativeSocketIoBenchmark { + @Param({"none", "wall=1s,filter=", "wall=1s,filter=,wallprecheck=true"}) + public String command; + + private JavaProfiler profiler; + private Path jfr; + private InetAddress loopback; + private ServerSocket serverSocket; + private Socket clientSocket; + private Socket serverSideSocket; + private InputStream clientInput; + private OutputStream clientOutput; + private InputStream serverInput; + private OutputStream serverOutput; + private Path file; + private FileInputStream fileInput; + private ServerSocket connectServerSocket; + private volatile boolean connectAcceptorRunning; + private Thread connectAcceptorThread; + private ServerSocket acceptServerSocket; + private volatile boolean acceptConnectorRunning; + private Thread acceptConnectorThread; + private DatagramSocket udpReceiverSocket; + private DatagramSocket udpSenderSocket; + private byte[] udpReceiveBuffer; + private byte[] udpSendBuffer; + private DatagramPacket udpReceivePacket; + private DatagramPacket udpSendPacket; + private Selector selector; + private Pipe selectorPipe; + private ByteBuffer selectorWriteBuffer; + private ByteBuffer selectorReadBuffer; + private final AtomicReference backgroundError = new AtomicReference<>(); + + @Setup(Level.Trial) + public void setup() throws IOException { + backgroundError.set(null); + if (!"none".equals(command)) { + profiler = JavaProfiler.getInstance(); + jfr = Files.createTempFile("native-socket-io-benchmark", ".jfr"); + profiler.execute("start," + command + ",jfr,file=" + jfr.toAbsolutePath()); + } + + loopback = InetAddress.getLoopbackAddress(); + + serverSocket = new ServerSocket(0, 1, loopback); + clientSocket = new Socket(loopback, serverSocket.getLocalPort()); + serverSideSocket = serverSocket.accept(); + clientSocket.setTcpNoDelay(true); + serverSideSocket.setTcpNoDelay(true); + clientInput = clientSocket.getInputStream(); + clientOutput = clientSocket.getOutputStream(); + serverInput = serverSideSocket.getInputStream(); + serverOutput = serverSideSocket.getOutputStream(); + + file = Files.createTempFile("native-socket-io-benchmark", ".bin"); + byte[] data = new byte[1024 * 1024]; + Files.write(file, data); + fileInput = new FileInputStream(file.toFile()); + + connectServerSocket = new ServerSocket(0, 50, loopback); + connectAcceptorRunning = true; + connectAcceptorThread = new Thread(this::acceptConnectBenchmarkSockets, + "native-io-connect-acceptor"); + connectAcceptorThread.setDaemon(true); + connectAcceptorThread.start(); + + acceptServerSocket = new ServerSocket(0, 50, loopback); + acceptConnectorRunning = true; + acceptConnectorThread = new Thread(this::connectAcceptBenchmarkSockets, + "native-io-accept-connector"); + acceptConnectorThread.setDaemon(true); + acceptConnectorThread.start(); + + udpReceiverSocket = new DatagramSocket(new InetSocketAddress(loopback, 0)); + udpSenderSocket = new DatagramSocket(); + udpReceiveBuffer = new byte[64]; + udpSendBuffer = new byte[]{1}; + udpReceivePacket = new DatagramPacket(udpReceiveBuffer, udpReceiveBuffer.length); + udpSendPacket = new DatagramPacket( + udpSendBuffer, udpSendBuffer.length, loopback, udpReceiverSocket.getLocalPort()); + + selector = Selector.open(); + selectorPipe = Pipe.open(); + selectorPipe.source().configureBlocking(false); + selectorPipe.source().register(selector, SelectionKey.OP_READ); + selectorWriteBuffer = ByteBuffer.allocate(1); + selectorReadBuffer = ByteBuffer.allocate(64); + } + + @TearDown(Level.Trial) + public void tearDown() throws IOException { + connectAcceptorRunning = false; + acceptConnectorRunning = false; + closeQuietly(connectServerSocket); + closeQuietly(acceptServerSocket); + joinQuietly(connectAcceptorThread); + joinQuietly(acceptConnectorThread); + closeQuietly(udpReceiverSocket); + closeQuietly(udpSenderSocket); + closeQuietly(selector); + if (selectorPipe != null) { + closeQuietly(selectorPipe.source()); + closeQuietly(selectorPipe.sink()); + } + closeQuietly(fileInput); + closeQuietly(clientSocket); + closeQuietly(serverSideSocket); + closeQuietly(serverSocket); + if (file != null) { + Files.deleteIfExists(file); + } + if (profiler != null) { + profiler.execute("stop"); + } + if (jfr != null) { + Files.deleteIfExists(jfr); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @Fork(value = 1, warmups = 1) + @Warmup(iterations = 3) + @Measurement(iterations = 5) + @Threads(1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public int socketRoundTrip() throws IOException { + clientOutput.write(1); + clientOutput.flush(); + int value = serverInput.read(); + serverOutput.write(value); + serverOutput.flush(); + return clientInput.read(); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @Fork(value = 1, warmups = 1) + @Warmup(iterations = 3) + @Measurement(iterations = 5) + @Threads(1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public int connectClose() throws IOException { + assertBackgroundHealthy(); + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(loopback, connectServerSocket.getLocalPort())); + return socket.getLocalPort(); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @Fork(value = 1, warmups = 1) + @Warmup(iterations = 3) + @Measurement(iterations = 5) + @Threads(1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public int acceptClose() throws IOException { + assertBackgroundHealthy(); + try (Socket accepted = acceptServerSocket.accept()) { + return accepted.getPort(); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @Fork(value = 1, warmups = 1) + @Warmup(iterations = 3) + @Measurement(iterations = 5) + @Threads(1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public int datagramReceive() throws IOException { + udpSendBuffer[0]++; + udpSenderSocket.send(udpSendPacket); + udpReceivePacket.setLength(udpReceiveBuffer.length); + udpReceiverSocket.receive(udpReceivePacket); + return udpReceivePacket.getLength(); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @Fork(value = 1, warmups = 1) + @Warmup(iterations = 3) + @Measurement(iterations = 5) + @Threads(1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public int selectorSelect() throws IOException { + selectorWriteBuffer.clear(); + selectorWriteBuffer.put((byte) 1); + selectorWriteBuffer.flip(); + while (selectorWriteBuffer.hasRemaining()) { + selectorPipe.sink().write(selectorWriteBuffer); + } + int selected = selector.select(1_000L); + selector.selectedKeys().clear(); + selectorReadBuffer.clear(); + while (selectorPipe.source().read(selectorReadBuffer) > 0) { + selectorReadBuffer.clear(); + } + return selected; + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @Fork(value = 1, warmups = 1) + @Warmup(iterations = 3) + @Measurement(iterations = 5) + @Threads(1) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public int regularFileRead() throws IOException { + int value = fileInput.read(); + if (value >= 0) { + return value; + } + fileInput.close(); + fileInput = new FileInputStream(file.toFile()); + return fileInput.read(); + } + + private void acceptConnectBenchmarkSockets() { + while (connectAcceptorRunning) { + try (Socket ignored = connectServerSocket.accept()) { + } catch (IOException e) { + if (connectAcceptorRunning) { + backgroundError.compareAndSet(null, e); + } + } + } + } + + private void connectAcceptBenchmarkSockets() { + while (acceptConnectorRunning) { + try (Socket ignored = new Socket(loopback, acceptServerSocket.getLocalPort())) { + } catch (IOException e) { + if (acceptConnectorRunning) { + backgroundError.compareAndSet(null, e); + } + } + } + } + + private void assertBackgroundHealthy() throws IOException { + Throwable failure = backgroundError.get(); + if (failure == null) { + return; + } + if (failure instanceof IOException) { + throw (IOException) failure; + } + throw new IOException(failure); + } + + private static void closeQuietly(AutoCloseable closeable) { + if (closeable == null) { + return; + } + try { + closeable.close(); + } catch (Exception ignored) { + } + } + + private static void joinQuietly(Thread thread) { + if (thread == null) { + return; + } + try { + thread.join(1_000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/J9WallClockPrecheckCapabilityTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/J9WallClockPrecheckCapabilityTest.java index f91eeef578..4abb50516d 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/J9WallClockPrecheckCapabilityTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/J9WallClockPrecheckCapabilityTest.java @@ -6,15 +6,27 @@ package com.datadoghq.profiler.wallclock; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.datadoghq.profiler.AbstractProfilerTest; import com.datadoghq.profiler.Platform; import com.datadoghq.profiler.ProfilerOwnedBlockHooks; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; +import org.openjdk.jmc.common.item.IItemCollection; /** Verifies that unsupported J9 wall sampling does not activate unfiltered precheck tracking. */ public class J9WallClockPrecheckCapabilityTest extends AbstractProfilerTest { private static final int OSTHREAD_STATE_SLEEPING = 7; + private static final int BLOCK_HOLD_MILLIS = 500; /** Ensures owned-block hooks stay inactive when the selected wall engine cannot consume them. */ @Test @@ -24,6 +36,35 @@ public void unsupportedEngineDoesNotActivateRegistry() { assertEquals(0L, token, "J9WallClock must not activate unfiltered precheck tracking"); } + /** Ensures unsupported precheck falls back to ordinary wall samples without a profiling gap. */ + @Test + public void blockingSocketReadFallsBackToMethodSample() throws Exception { + String workerName = "taskblock-j9-jvmti-fallback"; + Map before = profiler.getDebugCounters(); + + runBlockingSocketRead(workerName); + + Map after = profiler.getDebugCounters(); + assertEquals( + before.getOrDefault("task_block_emitted", 0L), + after.getOrDefault("task_block_emitted", 0L), + "J9WallClock must not emit TaskBlock events"); + assertEquals( + before.getOrDefault("wc_signals_suppressed_owned_block", 0L), + after.getOrDefault("wc_signals_suppressed_owned_block", 0L), + "J9WallClock must not suppress wall signals"); + + stopProfiler(); + IItemCollection taskBlocks = verifyEvents("datadog.TaskBlock", false); + assertFalse( + TaskBlockAssertions.containsEventThread(taskBlocks, workerName), + "J9WallClock fallback must not emit a TaskBlock for the worker"); + IItemCollection methodSamples = verifyEvents("datadog.MethodSample", false); + assertTrue( + TaskBlockAssertions.containsEventThread(methodSamples, workerName), + "J9WallClock fallback must retain wall-clock MethodSample coverage for the worker"); + } + @Override protected boolean isPlatformSupported() { return Platform.isJ9(); @@ -33,4 +74,41 @@ protected boolean isPlatformSupported() { protected String getProfilerCommand() { return "wall=1ms,wallsampler=jvmti,filter=,wallprecheck=true"; } + + private static void runBlockingSocketRead(String workerName) throws Exception { + CountDownLatch readAttempted = new CountDownLatch(1); + AtomicReference error = new AtomicReference<>(); + + try (ServerSocket server = new ServerSocket(0)) { + Thread reader = + new Thread( + () -> { + try (Socket socket = new Socket("127.0.0.1", server.getLocalPort())) { + InputStream input = socket.getInputStream(); + readAttempted.countDown(); + int value = input.read(); + if (value != 1) { + throw new AssertionError("unexpected socket byte: " + value); + } + } catch (Throwable t) { + error.set(t); + } + }, + workerName); + + reader.start(); + try (Socket accepted = server.accept()) { + assertTrue(readAttempted.await(5, TimeUnit.SECONDS), "reader did not enter socket read"); + Thread.sleep(BLOCK_HOLD_MILLIS); + OutputStream output = accepted.getOutputStream(); + output.write(1); + output.flush(); + } + reader.join(5_000L); + assertFalse(reader.isAlive(), "socket reader did not complete"); + if (error.get() != null) { + throw new AssertionError(error.get()); + } + } + } } diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/JvmtiBasedNativeSocketTaskBlockTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/JvmtiBasedNativeSocketTaskBlockTest.java new file mode 100644 index 0000000000..f8a77e856f --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/JvmtiBasedNativeSocketTaskBlockTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.datadoghq.profiler.wallclock; + +import com.datadoghq.profiler.Platform; +import org.junit.jupiter.api.Assumptions; + +import java.util.Map; + +/** Verifies synchronous native-I/O production when delegated wall-clock stacks are enabled. */ +public class JvmtiBasedNativeSocketTaskBlockTest extends NativeSocketTaskBlockTest { + @Override + protected void before() throws Exception { + Map counters = profiler.getDebugCounters(); + Assumptions.assumeTrue( + counters.getOrDefault("jvmti_stacks_init_ok", 0L) > 0, + "HotSpot RequestStackTrace JVMTI extension is not available"); + } + + @Override + protected void withTestAssumptions() { + Assumptions.assumeTrue(Platform.isJavaVersionAtLeast(11)); + } + + @Override + protected String getProfilerCommand() { + return "wall=1ms,filter=,wallprecheck=true,jvmtistacks=true"; + } +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/NativeSocketTaskBlockLifecycleTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/NativeSocketTaskBlockLifecycleTest.java new file mode 100644 index 0000000000..09da8c6034 --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/NativeSocketTaskBlockLifecycleTest.java @@ -0,0 +1,259 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.datadoghq.profiler.wallclock; + +import com.datadoghq.profiler.AbstractProfilerTest; +import com.datadoghq.profiler.Platform; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.openjdk.jmc.common.item.IItemCollection; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** Verifies native I/O hooks are fully restored and can be reinstalled across profiler restarts. */ +public class NativeSocketTaskBlockLifecycleTest extends AbstractProfilerTest { + private static final int BLOCK_HOLD_MILLIS = 250; + private static final int NATIVE_BLOCK_ATTEMPTS = 5; + + @BeforeAll + static void preloadJdkNetworking() throws Exception { + if (Platform.isLinux()) { + try (ServerSocket server = new ServerSocket(0); + Socket client = new Socket("127.0.0.1", server.getLocalPort()); + Socket accepted = server.accept()) { + // Exercise Java networking before profiler start so the native libraries + // needed by socket reads are loaded before the initial patch pass. + } + } + } + + @Test + public void restartWithWallPrecheckDisabledStopsNativeSocketTaskBlocks() throws Exception { + String enabledWorkerName = "taskblock-native-lifecycle-enabled"; + for (int attempt = 0; attempt < NATIVE_BLOCK_ATTEMPTS; attempt++) { + runCompletedSocketRead(enabledWorkerName); + } + stopProfiler(); + assertIoWaitTaskBlockPresent( + verifyEvents("datadog.TaskBlock", false), enabledWorkerName); + + Path disabledRecording = Files.createTempFile(Paths.get("/tmp/recordings"), + "NativeSocketTaskBlockLifecycleTest_disabled_", ".jfr"); + boolean disabledRunning = false; + try { + profiler.execute("start,wall=1ms,filter=,wallprecheck=false,jfr,file=" + + disabledRecording.toAbsolutePath()); + disabledRunning = true; + String disabledWorkerName = "taskblock-native-lifecycle-disabled"; + for (int attempt = 0; attempt < NATIVE_BLOCK_ATTEMPTS; attempt++) { + runCompletedSocketRead(disabledWorkerName); + } + profiler.stop(); + disabledRunning = false; + + IItemCollection disabledTaskBlocks = + verifyEvents(disabledRecording, "datadog.TaskBlock", false); + assertFalse(TaskBlockAssertions.containsEventThread( + disabledTaskBlocks, disabledWorkerName), + "wallprecheck=false restart must not emit a socket TaskBlock for the worker"); + } finally { + if (disabledRunning) { + profiler.stop(); + } + Files.deleteIfExists(disabledRecording); + } + + Path reenabledRecording = Files.createTempFile(Paths.get("/tmp/recordings"), + "NativeSocketTaskBlockLifecycleTest_reenabled_", ".jfr"); + boolean reenabledRunning = false; + try { + profiler.execute("start,wall=1ms,filter=,wallprecheck=true,jfr,file=" + + reenabledRecording.toAbsolutePath()); + reenabledRunning = true; + String reenabledWorkerName = "taskblock-native-lifecycle-reenabled"; + for (int attempt = 0; attempt < NATIVE_BLOCK_ATTEMPTS; attempt++) { + runCompletedSocketRead(reenabledWorkerName); + } + profiler.stop(); + reenabledRunning = false; + + assertIoWaitTaskBlockPresent(verifyEvents( + reenabledRecording, "datadog.TaskBlock", false), reenabledWorkerName); + } finally { + if (reenabledRunning) { + profiler.stop(); + } + Files.deleteIfExists(reenabledRecording); + } + } + + @Test + public void stopWhileSocketReadIsBlockedCleansUpAndAllowsReinstall() throws Exception { + String workerName = "taskblock-native-stop-inflight"; + CountDownLatch readAttempted = new CountDownLatch(1); + AtomicReference error = new AtomicReference<>(); + ServerSocket server = new ServerSocket(0); + Socket accepted = null; + boolean stopped = false; + + Thread reader = new Thread(() -> { + try (Socket socket = new Socket("127.0.0.1", server.getLocalPort())) { + InputStream input = socket.getInputStream(); + readAttempted.countDown(); + input.read(); + } catch (Throwable t) { + error.set(t); + } + }, workerName); + + try { + reader.start(); + accepted = server.accept(); + assertTrue(readAttempted.await(5, TimeUnit.SECONDS), + "reader did not attempt the blocking socket read"); + Thread.sleep(BLOCK_HOLD_MILLIS); + assertTrue(reader.isAlive(), + "socket read returned before the profiler was stopped"); + + stopProfiler(); + stopped = true; + assertTrue(reader.isAlive(), + "socket read returned before profiler shutdown completed"); + } finally { + if (!stopped) { + stopProfiler(); + } + if (accepted != null) { + accepted.close(); + } + server.close(); + reader.join(5_000L); + } + + assertFalse(reader.isAlive(), "blocked socket reader did not terminate"); + if (error.get() != null) { + throw new AssertionError(error.get()); + } + assertFalse(TaskBlockAssertions.containsObservedStateForEventThread( + verifyEvents("datadog.TaskBlock", false), "IO_WAIT", workerName), + "a native I/O operation returning after stop must not emit TaskBlock"); + + Path restartedRecording = Files.createTempFile(Paths.get("/tmp/recordings"), + "NativeSocketTaskBlockLifecycleTest_inflight_restart_", ".jfr"); + boolean restarted = false; + try { + profiler.execute("start,wall=1ms,filter=,wallprecheck=true,jfr,file=" + + restartedRecording.toAbsolutePath()); + restarted = true; + String restartedWorkerName = "taskblock-native-stop-reinstalled"; + for (int attempt = 0; attempt < NATIVE_BLOCK_ATTEMPTS; attempt++) { + runCompletedSocketRead(restartedWorkerName); + } + profiler.stop(); + restarted = false; + + IItemCollection restartedTaskBlocks = + verifyEvents(restartedRecording, "datadog.TaskBlock", false); + TaskBlockAssertions.assertNoAnchorFields(restartedTaskBlocks); + TaskBlockAssertions.assertContainsStackTrace(restartedTaskBlocks); + TaskBlockAssertions.assertContainsJavaType( + restartedTaskBlocks, "NativeSocketTaskBlockLifecycleTest"); + assertTrue(TaskBlockAssertions.containsObservedStateForEventThread( + restartedTaskBlocks, "IO_WAIT", restartedWorkerName), + "socket TaskBlock hooks were not reinstalled after profiler shutdown"); + } finally { + if (restarted) { + profiler.stop(); + } + Files.deleteIfExists(restartedRecording); + } + } + + @Override + protected boolean isPlatformSupported() { + return Platform.isLinux(); + } + + @Override + protected String getProfilerCommand() { + return "wall=1ms,filter=,wallprecheck=true"; + } + + private void assertIoWaitTaskBlockPresent( + IItemCollection taskBlockEvents, String workerName) { + if (!taskBlockEvents.hasItems()) { + fail(missingTaskBlockDiagnostic()); + } + TaskBlockAssertions.assertNoAnchorFields(taskBlockEvents); + TaskBlockAssertions.assertContainsStackTrace(taskBlockEvents); + TaskBlockAssertions.assertContainsJavaType( + taskBlockEvents, "NativeSocketTaskBlockLifecycleTest"); + TaskBlockAssertions.assertNoCorrelationId(taskBlockEvents); + TaskBlockAssertions.assertContainsObservedState(taskBlockEvents, "IO_WAIT"); + assertTrue(TaskBlockAssertions.containsObservedStateForEventThread( + taskBlockEvents, "IO_WAIT", workerName), + "Expected native IO_WAIT TaskBlock for " + workerName); + } + + private void runCompletedSocketRead(String workerName) throws Exception { + CountDownLatch readAttempted = new CountDownLatch(1); + AtomicReference error = new AtomicReference<>(); + + try (ServerSocket server = new ServerSocket(0)) { + Thread reader = new Thread(() -> { + try (Socket socket = new Socket("127.0.0.1", server.getLocalPort())) { + InputStream input = socket.getInputStream(); + readAttempted.countDown(); + int value = input.read(); + if (value != 1) { + throw new AssertionError("unexpected socket byte: " + value); + } + } catch (Throwable t) { + error.set(t); + } + }, workerName); + + reader.start(); + try (Socket accepted = server.accept()) { + assertTrue(readAttempted.await(5, TimeUnit.SECONDS), + "reader did not attempt the blocking socket read"); + Thread.sleep(BLOCK_HOLD_MILLIS); + OutputStream output = accepted.getOutputStream(); + output.write(1); + output.flush(); + } + reader.join(5_000L); + assertFalse(reader.isAlive(), "socket reader did not complete"); + if (error.get() != null) { + throw new AssertionError(error.get()); + } + } + } + + private String missingTaskBlockDiagnostic() { + return "Expected lifecycle native TaskBlock after " + NATIVE_BLOCK_ATTEMPTS + + " blocked interval(s); emitted=" + getRecordedCounterValue("task_block_emitted") + + ", stack_capture_failed=" + + getRecordedCounterValue("task_block_stack_capture_failed") + + ", skipped_too_short=" + getRecordedCounterValue("task_block_skipped_too_short") + + ", skipped_trace_context=" + + getRecordedCounterValue("task_block_skipped_trace_context") + + ", record_failed=" + getRecordedCounterValue("task_block_record_failed"); + } +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/NativeSocketTaskBlockTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/NativeSocketTaskBlockTest.java new file mode 100644 index 0000000000..9c62e175b6 --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/NativeSocketTaskBlockTest.java @@ -0,0 +1,339 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.datadoghq.profiler.wallclock; + +import com.datadoghq.profiler.AbstractProfilerTest; +import com.datadoghq.profiler.Platform; +import org.junit.jupiter.api.Test; +import org.openjdk.jmc.common.item.IItemCollection; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.Pipe; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Verifies Linux native socket and readiness waits produce self-contained TaskBlock events. */ +public class NativeSocketTaskBlockTest extends AbstractProfilerTest { + private static final int BLOCK_HOLD_MILLIS = 250; + private static final int NATIVE_BLOCK_ATTEMPTS = 5; + + @Test + public void blockingSocketReadEmitsIoWaitTaskBlock() throws Exception { + for (int attempt = 0; attempt < NATIVE_BLOCK_ATTEMPTS; attempt++) { + runBlockingSocketReadOnce(); + } + + stopProfiler(); + assertIoWaitTaskBlockSelfContained("taskblock-native-socket-read"); + } + + private void runBlockingSocketReadOnce() throws Exception { + CountDownLatch readAttempted = new CountDownLatch(1); + AtomicReference error = new AtomicReference<>(); + + try (ServerSocket server = new ServerSocket(0)) { + Thread reader = new Thread(() -> { + try { + try (Socket socket = new Socket("127.0.0.1", server.getLocalPort())) { + InputStream input = socket.getInputStream(); + readAttempted.countDown(); + int value = input.read(); + if (value != 1) { + throw new AssertionError("unexpected socket byte: " + value); + } + } + } catch (Throwable t) { + error.set(t); + } + }, "taskblock-native-socket-read"); + + reader.start(); + try (Socket accepted = server.accept()) { + assertTrue(readAttempted.await(5, TimeUnit.SECONDS), "reader did not enter socket read"); + Thread.sleep(BLOCK_HOLD_MILLIS); + OutputStream output = accepted.getOutputStream(); + output.write(1); + output.flush(); + } + assertCompleted(reader, error); + } + } + + @Test + public void blockingServerSocketAcceptEmitsIoWaitTaskBlock() throws Exception { + for (int attempt = 0; attempt < NATIVE_BLOCK_ATTEMPTS; attempt++) { + runBlockingServerSocketAcceptOnce(); + } + + stopProfiler(); + assertIoWaitTaskBlockSelfContained("taskblock-native-socket-accept"); + } + + private void runBlockingServerSocketAcceptOnce() throws Exception { + CountDownLatch acceptAttempted = new CountDownLatch(1); + AtomicReference error = new AtomicReference<>(); + InetAddress loopback = InetAddress.getLoopbackAddress(); + + try (ServerSocket server = new ServerSocket(0, 1, loopback)) { + Thread accepter = new Thread(() -> { + try { + acceptAttempted.countDown(); + try (Socket accepted = server.accept()) { + assertTrue(accepted.isConnected()); + } + } catch (Throwable t) { + error.set(t); + } + }, "taskblock-native-socket-accept"); + + accepter.start(); + assertTrue(acceptAttempted.await(5, TimeUnit.SECONDS), "accept did not start"); + Thread.sleep(BLOCK_HOLD_MILLIS); + try (Socket ignored = new Socket(loopback, server.getLocalPort())) { + assertTrue(ignored.isConnected()); + } + assertCompleted(accepter, error); + } + } + + @Test + public void blockingDatagramReceiveEmitsIoWaitTaskBlock() throws Exception { + for (int attempt = 0; attempt < NATIVE_BLOCK_ATTEMPTS; attempt++) { + runBlockingDatagramReceiveOnce(); + } + + stopProfiler(); + assertIoWaitTaskBlockSelfContained("taskblock-native-datagram-receive"); + } + + private void runBlockingDatagramReceiveOnce() throws Exception { + CountDownLatch receiveAttempted = new CountDownLatch(1); + AtomicReference error = new AtomicReference<>(); + InetAddress loopback = InetAddress.getLoopbackAddress(); + + try (DatagramSocket receiver = new DatagramSocket(new InetSocketAddress(loopback, 0))) { + Thread receiverThread = new Thread(() -> { + try { + byte[] data = new byte[1]; + DatagramPacket packet = new DatagramPacket(data, data.length); + receiveAttempted.countDown(); + receiver.receive(packet); + assertEquals(1, packet.getLength()); + assertEquals(7, data[0]); + } catch (Throwable t) { + error.set(t); + } + }, "taskblock-native-datagram-receive"); + + receiverThread.start(); + assertTrue(receiveAttempted.await(5, TimeUnit.SECONDS), "receive did not start"); + Thread.sleep(BLOCK_HOLD_MILLIS); + try (DatagramSocket sender = new DatagramSocket()) { + byte[] data = new byte[]{7}; + DatagramPacket packet = new DatagramPacket( + data, data.length, loopback, receiver.getLocalPort()); + sender.send(packet); + } + assertCompleted(receiverThread, error); + } + } + + @Test + public void blockingSelectorSelectEmitsIoWaitTaskBlock() throws Exception { + String workerName = "taskblock-native-selector-select"; + long suppressedBefore = profiler.getDebugCounters() + .getOrDefault("wc_signals_suppressed_owned_block", 0L); + runBlockingSelectorSelectOnce(); + long suppressedAfter = profiler.getDebugCounters() + .getOrDefault("wc_signals_suppressed_owned_block", 0L); + assertTrue(suppressedAfter > suppressedBefore, + "Expected wall signals to be suppressed during the native selector wait"); + + stopProfiler(); + IItemCollection taskBlocks = verifyEvents("datadog.TaskBlock", false); + assertIoWaitTaskBlockSelfContained(taskBlocks, workerName); + int workerEvents = TaskBlockAssertions.countEventsForThread(taskBlocks, workerName); + assertTrue(workerEvents >= 1 && workerEvents <= 2, + "Expected one logical selector wait to produce at most two TaskBlocks, got: " + + workerEvents); + } + + private void runBlockingSelectorSelectOnce() throws Exception { + CountDownLatch selectAttempted = new CountDownLatch(1); + AtomicReference error = new AtomicReference<>(); + + Pipe pipe = Pipe.open(); + try (Selector selector = Selector.open(); + Pipe.SourceChannel source = pipe.source(); + Pipe.SinkChannel sink = pipe.sink()) { + source.configureBlocking(false); + source.register(selector, SelectionKey.OP_READ); + + Thread selectorThread = new Thread(() -> { + try { + selectAttempted.countDown(); + int selected = selectUntilReady(selector, 5_000L); + assertTrue(selected > 0, "selector did not observe pipe readiness"); + selector.selectedKeys().clear(); + ByteBuffer data = ByteBuffer.allocate(1); + while (data.hasRemaining() && source.read(data) > 0) { + } + } catch (Throwable t) { + error.set(t); + } + }, "taskblock-native-selector-select"); + + selectorThread.start(); + assertTrue(selectAttempted.await(5, TimeUnit.SECONDS), "select did not start"); + Thread.sleep(BLOCK_HOLD_MILLIS); + sink.write(ByteBuffer.wrap(new byte[]{1})); + assertCompleted(selectorThread, error); + } + } + + @Test + public void tracedBlockingSocketReadDoesNotEmitTaskBlock() throws Exception { + String workerName = "taskblock-traced-native-socket-read"; + long skippedBefore = profiler.getDebugCounters() + .getOrDefault("task_block_skipped_trace_context", 0L); + CountDownLatch readAttempted = new CountDownLatch(1); + AtomicReference error = new AtomicReference<>(); + + try (ServerSocket server = new ServerSocket(0)) { + Thread reader = new Thread(() -> { + try { + try (Socket socket = new Socket("127.0.0.1", server.getLocalPort())) { + InputStream input = socket.getInputStream(); + profiler.setContext(0x5100L, 0x5101L, 0L, 0x5101L); + try { + readAttempted.countDown(); + int value = input.read(); + if (value != 1) { + throw new AssertionError("unexpected socket byte: " + value); + } + } finally { + profiler.clearContext(); + } + } + } catch (Throwable t) { + error.set(t); + } + }, workerName); + + reader.start(); + try (Socket accepted = server.accept()) { + assertTrue(readAttempted.await(5, TimeUnit.SECONDS), "reader did not enter socket read"); + Thread.sleep(BLOCK_HOLD_MILLIS); + OutputStream output = accepted.getOutputStream(); + output.write(1); + output.flush(); + } + assertCompleted(reader, error); + } + + assertTrue(profiler.getDebugCounters() + .getOrDefault("task_block_skipped_trace_context", 0L) > skippedBefore, + "Native socket hook did not reject the traced blocking read"); + stopProfiler(); + IItemCollection taskBlocks = verifyEvents("datadog.TaskBlock", false); + assertFalse( + TaskBlockAssertions.containsEventThread(taskBlocks, workerName), + "Traced socket I/O must not emit a TaskBlock for the worker"); + IItemCollection methodSamples = verifyEvents("datadog.MethodSample", false); + assertTrue(TaskBlockAssertions.containsEventThread(methodSamples, workerName), + "Traced socket I/O must retain MethodSample wall-clock data for the worker"); + assertTrue(TaskBlockAssertions.containsSpan(methodSamples, 0x5101L), + "Traced socket MethodSample must retain its span context"); + } + + @Override + protected boolean isPlatformSupported() { + return Platform.isLinux(); + } + + @Override + protected String getProfilerCommand() { + return "wall=1ms,filter=,wallprecheck=true"; + } + + private static int selectUntilReady(Selector selector, long timeoutMillis) throws Exception { + long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis); + int selected; + do { + long remainingNanos = deadline - System.nanoTime(); + if (remainingNanos <= 0L) { + return 0; + } + selected = selector.select(Math.max(1L, TimeUnit.NANOSECONDS.toMillis(remainingNanos))); + } while (selected == 0); + return selected; + } + + protected void assertIoWaitTaskBlockSelfContained(String workerName) { + assertIoWaitTaskBlockSelfContained( + verifyEvents("datadog.TaskBlock", false), workerName); + } + + private void assertIoWaitTaskBlockSelfContained( + IItemCollection taskBlockEvents, String workerName) { + assertNativeTaskBlockPresent(taskBlockEvents); + TaskBlockAssertions.assertNoAnchorFields(taskBlockEvents); + assertTaskBlockStackReference(taskBlockEvents); + TaskBlockAssertions.assertContainsObservedState(taskBlockEvents, "IO_WAIT"); + assertTrue(TaskBlockAssertions.containsObservedStateForEventThread( + taskBlockEvents, "IO_WAIT", workerName), + "Expected native IO_WAIT TaskBlock for " + workerName); + } + + protected void assertTaskBlockStackReference(IItemCollection taskBlockEvents) { + TaskBlockAssertions.assertContainsStackTrace(taskBlockEvents); + TaskBlockAssertions.assertContainsJavaType(taskBlockEvents, "NativeSocketTaskBlockTest"); + TaskBlockAssertions.assertNoCorrelationId(taskBlockEvents); + } + + private void assertNativeTaskBlockPresent(IItemCollection taskBlockEvents) { + if (!taskBlockEvents.hasItems()) { + String diagnostic = missingTaskBlockDiagnostic(); + System.out.println(diagnostic); + assertTrue(false, diagnostic); + } + } + + private String missingTaskBlockDiagnostic() { + return "Expected native socket TaskBlock after " + NATIVE_BLOCK_ATTEMPTS + + " blocked interval(s); emitted=" + getRecordedCounterValue("task_block_emitted") + + ", stack_capture_failed=" + + getRecordedCounterValue("task_block_stack_capture_failed") + + ", skipped_too_short=" + getRecordedCounterValue("task_block_skipped_too_short") + + ", skipped_trace_context=" + + getRecordedCounterValue("task_block_skipped_trace_context") + + ", record_failed=" + getRecordedCounterValue("task_block_record_failed"); + } + + private static void assertCompleted(Thread thread, AtomicReference error) + throws InterruptedException { + thread.join(5_000L); + assertFalse(thread.isAlive(), thread.getName() + " did not complete"); + if (error.get() != null) { + throw new AssertionError(error.get()); + } + } +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/OpenJ9NativeSocketTaskBlockTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/OpenJ9NativeSocketTaskBlockTest.java new file mode 100644 index 0000000000..b36828d934 --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/OpenJ9NativeSocketTaskBlockTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.datadoghq.profiler.wallclock; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.datadoghq.profiler.AbstractProfilerTest; +import com.datadoghq.profiler.Platform; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import org.openjdk.jmc.common.item.IItemCollection; + +/** Verifies that OpenJ9's ASGCT wall engine supports native socket TaskBlock production. */ +public class OpenJ9NativeSocketTaskBlockTest extends AbstractProfilerTest { + private static final int BLOCK_HOLD_MILLIS = 250; + + /** Requires hook installation, signal suppression, and a self-contained worker TaskBlock. */ + @Test + public void asgctWallEngineEmitsNativeSocketTaskBlock() throws Exception { + String workerName = "taskblock-openj9-native-socket-read"; + Map before = profiler.getDebugCounters(); + assertExpectedHookPath(before); + + runBlockingSocketRead(workerName); + + Map after = profiler.getDebugCounters(); + assertTrue( + after.getOrDefault("task_block_emitted", 0L) + > before.getOrDefault("task_block_emitted", 0L), + "OpenJ9 native socket read did not emit a TaskBlock"); + assertTrue( + after.getOrDefault("wc_signals_suppressed_owned_block", 0L) + > before.getOrDefault("wc_signals_suppressed_owned_block", 0L), + "OpenJ9 ASGCT wall precheck did not suppress the owned native block"); + assertEquals( + before.getOrDefault("task_block_stack_capture_failed", 0L), + after.getOrDefault("task_block_stack_capture_failed", 0L), + "OpenJ9 TaskBlock stack capture failed"); + assertEquals( + before.getOrDefault("task_block_record_failed", 0L), + after.getOrDefault("task_block_record_failed", 0L), + "OpenJ9 TaskBlock recording failed"); + + stopProfiler(); + IItemCollection taskBlocks = verifyEvents("datadog.TaskBlock", false); + assertTrue( + TaskBlockAssertions.containsObservedStateForEventThread( + taskBlocks, "IO_WAIT", workerName), + "Expected an OpenJ9 native IO_WAIT TaskBlock for " + workerName); + TaskBlockAssertions.assertContainsStackTrace(taskBlocks); + TaskBlockAssertions.assertContainsJavaType(taskBlocks, "OpenJ9NativeSocketTaskBlockTest"); + TaskBlockAssertions.assertNoCorrelationId(taskBlocks); + } + + @Override + protected boolean isPlatformSupported() { + return Platform.isLinux() && Platform.isJ9(); + } + + @Override + protected String getProfilerCommand() { + return "wall=1ms,filter=,wallprecheck=true"; + } + + private static void assertExpectedHookPath(Map counters) { + long standardHooks = counters.getOrDefault("native_io_standard_hooks_patched", 0L); + long ibmBridgeHooks = counters.getOrDefault("native_io_ibm_bridge_hooks_patched", 0L); + if (isLegacyIbmJ9()) { + assertTrue( + ibmBridgeHooks > 0, + "Legacy IBM J9 must install native I/O hooks in the IBM JCL bridge; " + + hookDiagnostic(standardHooks, ibmBridgeHooks)); + } else { + assertTrue( + standardHooks > 0, + "Semeru/OpenJ9 must install native I/O hooks in standard JDK networking libraries; " + + hookDiagnostic(standardHooks, ibmBridgeHooks)); + } + } + + private static boolean isLegacyIbmJ9() { + String vmName = System.getProperty("java.vm.name", ""); + String fullVersion = System.getProperty("java.fullversion", ""); + return vmName.contains("IBM J9") || fullVersion.startsWith("JRE 1.8.0 IBM"); + } + + private static String hookDiagnostic(long standardHooks, long ibmBridgeHooks) { + return "java.vm.name=" + + System.getProperty("java.vm.name", "") + + ", java.fullversion=" + + System.getProperty("java.fullversion", "") + + ", standard hooks=" + + standardHooks + + ", IBM bridge hooks=" + + ibmBridgeHooks; + } + + private static void runBlockingSocketRead(String workerName) throws Exception { + CountDownLatch readAttempted = new CountDownLatch(1); + AtomicReference error = new AtomicReference<>(); + + try (ServerSocket server = new ServerSocket(0)) { + Thread reader = + new Thread( + () -> { + try (Socket socket = new Socket("127.0.0.1", server.getLocalPort())) { + InputStream input = socket.getInputStream(); + readAttempted.countDown(); + int value = input.read(); + if (value != 1) { + throw new AssertionError("unexpected socket byte: " + value); + } + } catch (Throwable t) { + error.set(t); + } + }, + workerName); + + reader.start(); + try (Socket accepted = server.accept()) { + assertTrue(readAttempted.await(5, TimeUnit.SECONDS), "reader did not enter socket read"); + Thread.sleep(BLOCK_HOLD_MILLIS); + OutputStream output = accepted.getOutputStream(); + output.write(1); + output.flush(); + } + reader.join(5_000L); + assertFalse(reader.isAlive(), "socket reader did not complete"); + if (error.get() != null) { + throw new AssertionError(error.get()); + } + } + } +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/TaskBlockAssertions.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/TaskBlockAssertions.java index e5e6f33c31..35ba41bdf8 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/TaskBlockAssertions.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/TaskBlockAssertions.java @@ -10,13 +10,17 @@ import java.util.Set; import org.openjdk.jmc.common.IMCFrame; import org.openjdk.jmc.common.IMCStackTrace; +import org.openjdk.jmc.common.IMCThread; import org.openjdk.jmc.common.item.IAttribute; import org.openjdk.jmc.common.item.IItem; import org.openjdk.jmc.common.item.IItemCollection; import org.openjdk.jmc.common.item.IItemIterable; import org.openjdk.jmc.common.item.IMemberAccessor; import org.openjdk.jmc.common.unit.IQuantity; +import org.openjdk.jmc.flightrecorder.JfrAttributes; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.openjdk.jmc.common.item.Attribute.attr; @@ -142,6 +146,86 @@ static boolean containsBlocker(IItemCollection events, long blocker) { return false; } + static boolean containsObservedStateForEventThread( + IItemCollection events, String observedState, String threadName) { + for (IItemIterable iterable : events) { + IMemberAccessor stateAccessor = + OBSERVED_BLOCKING_STATE.getAccessor(iterable.getType()); + IMemberAccessor threadAccessor = + JfrAttributes.EVENT_THREAD.getAccessor(iterable.getType()); + if (stateAccessor == null || threadAccessor == null) continue; + for (IItem item : iterable) { + IMCThread thread = threadAccessor.getMember(item); + if (observedState.equals(stateAccessor.getMember(item)) + && thread != null + && threadName.equals(thread.getThreadName())) { + return true; + } + } + } + return false; + } + + static boolean containsEventThread(IItemCollection events, String threadName) { + for (IItemIterable iterable : events) { + IMemberAccessor threadAccessor = + JfrAttributes.EVENT_THREAD.getAccessor(iterable.getType()); + if (threadAccessor == null) continue; + for (IItem item : iterable) { + IMCThread thread = threadAccessor.getMember(item); + if (thread != null && threadName.equals(thread.getThreadName())) return true; + } + } + return false; + } + + static int countEventsForThread(IItemCollection events, String threadName) { + int count = 0; + for (IItemIterable iterable : events) { + IMemberAccessor threadAccessor = + JfrAttributes.EVENT_THREAD.getAccessor(iterable.getType()); + if (threadAccessor == null) continue; + for (IItem item : iterable) { + IMCThread thread = threadAccessor.getMember(item); + if (thread != null && threadName.equals(thread.getThreadName())) count++; + } + } + return count; + } + + static boolean containsSpan(IItemCollection events, long spanId) { + for (IItemIterable iterable : events) { + IMemberAccessor spanAccessor = + AbstractProfilerTest.SPAN_ID.getAccessor(iterable.getType()); + if (spanAccessor == null) continue; + for (IItem item : iterable) { + if (spanAccessor.getMember(item).longValue() == spanId) return true; + } + } + return false; + } + + static void assertBlockerEventThreadDiffers( + IItemCollection events, long blocker, long logicalThreadId) { + int checked = 0; + for (IItemIterable iterable : events) { + IMemberAccessor blockerAccessor = + BLOCKER.getAccessor(iterable.getType()); + IMemberAccessor threadAccessor = + JfrAttributes.EVENT_THREAD.getAccessor(iterable.getType()); + if (blockerAccessor == null || threadAccessor == null) continue; + for (IItem item : iterable) { + if (blockerAccessor.getMember(item).longValue() != blocker) continue; + IMCThread eventThread = threadAccessor.getMember(item); + assertNotNull(eventThread, "TaskBlock eventThread must not be null"); + assertNotEquals(Long.valueOf(logicalThreadId), eventThread.getThreadId(), + "Native TaskBlock must identify the physical carrier, not the virtual thread"); + checked++; + } + } + assertTrue(checked > 0, "Expected TaskBlock eventThread for blocker=" + blocker); + } + static void assertNoAnchorFields(IItemCollection events) { for (IItemIterable iterable : events) { assertNull(ANCHOR_SAMPLE_ID.getAccessor(iterable.getType()));