Skip to content

Commit 0ec65ff

Browse files
jbachorikclaude
andauthored
Intercept sigaction to prevent libraries from overwriting signal handlers (#420)
* Intercept sigaction to prevent wasmtime from overwriting signal handlers Wasmtime's SIGSEGV handler calls malloc() via __tls_get_addr, which is not async-signal-safe and causes deadlocks when profiler uses safefetch. Patch wasmtime's sigaction GOT entry to intercept handler installations. Their handlers are stored as chain targets and called from our handlers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Track and warn when native library limit is exceeded - Add NATIVE_LIBS_DROPPED counter and LOG_WARN macro - CodeCacheArray::add() returns bool, logs once on overflow - Fix memory leak: delete CodeCache on failed add - Add missing macOS stubs for sigaction protection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2 parents dbae23b + e35fff9 commit 0ec65ff

14 files changed

Lines changed: 359 additions & 19 deletions

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,11 @@ void CodeCache::addImport(void **entry, const char *name) {
338338
saveImport(im_realloc, entry);
339339
}
340340
break;
341+
case 's':
342+
if (strcmp(name, "sigaction") == 0) {
343+
saveImport(im_sigaction, entry);
344+
}
345+
break;
341346
}
342347
}
343348

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#ifndef _CODECACHE_H
77
#define _CODECACHE_H
88

9+
#include "common.h"
10+
#include "counters.h"
911
#include "utils.h"
1012

1113
#include <jvmti.h>
@@ -31,6 +33,7 @@ enum ImportId {
3133
im_calloc,
3234
im_realloc,
3335
im_free,
36+
im_sigaction,
3437
NUM_IMPORTS
3538
};
3639

@@ -257,9 +260,10 @@ class CodeCacheArray {
257260
volatile int _reserved; // next slot to reserve (CAS by writers)
258261
volatile int _count; // published count (all indices < _count have non-NULL pointers)
259262
volatile size_t _used_memory;
263+
bool _overflow_reported;
260264

261265
public:
262-
CodeCacheArray() : _reserved(0), _count(0), _used_memory(0) {
266+
CodeCacheArray() : _reserved(0), _count(0), _used_memory(0), _overflow_reported(false) {
263267
memset(_libs, 0, MAX_NATIVE_LIBS * sizeof(CodeCache *));
264268
}
265269

@@ -271,10 +275,17 @@ class CodeCacheArray {
271275
// Pointer-first add: reserve a slot via CAS on _reserved, store the
272276
// pointer with RELEASE, then advance _count. Readers see count() grow
273277
// only after the pointer is visible, so indices < count() never yield NULL.
274-
void add(CodeCache *lib) {
278+
bool add(CodeCache *lib) {
275279
int slot = __atomic_load_n(&_reserved, __ATOMIC_RELAXED);
276280
do {
277-
if (slot >= MAX_NATIVE_LIBS) return;
281+
if (slot >= MAX_NATIVE_LIBS) {
282+
Counters::increment(NATIVE_LIBS_DROPPED);
283+
if (!_overflow_reported) {
284+
_overflow_reported = true;
285+
LOG_WARN("Native library limit reached (%d). Additional libraries will not be tracked.", MAX_NATIVE_LIBS);
286+
}
287+
return false;
288+
}
278289
} while (!__atomic_compare_exchange_n(&_reserved, &slot, slot + 1,
279290
true, __ATOMIC_RELAXED, __ATOMIC_RELAXED));
280291
assert(__atomic_load_n(&_libs[slot], __ATOMIC_RELAXED) == nullptr);
@@ -288,6 +299,7 @@ class CodeCacheArray {
288299
// wait for preceding slots to publish
289300
}
290301
__atomic_store_n(&_count, slot + 1, __ATOMIC_RELEASE);
302+
return true;
291303
}
292304

293305
CodeCache* at(int index) const {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#define _COMMON_H
33

44
#include <cstddef>
5+
#include <cstdio>
56

67
// Knuth's multiplicative constant (golden ratio * 2^64 for 64-bit)
78
// Used for hash distribution in various components
@@ -16,4 +17,10 @@ constexpr size_t KNUTH_MULTIPLICATIVE_CONSTANT = 0x9e3779b97f4a7c15ULL;
1617
#define TEST_LOG(fmt, ...) // No-op in non-debug mode
1718
#endif
1819

20+
// Lightweight stderr warning that does not depend on the Log subsystem.
21+
// Safe to call from low-level code where Log may not be initialized.
22+
#define LOG_WARN(fmt, ...) do { \
23+
fprintf(stderr, "[ddprof] [WARN] " fmt "\n", ##__VA_ARGS__); \
24+
} while (0)
25+
1926
#endif // _COMMON_H

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,10 @@
9292
X(WALKVM_STUB_FRAMESIZE_FALLBACK, "walkvm_stub_framesize_fallback") \
9393
X(WALKVM_FP_CHAIN_ATTEMPT, "walkvm_fp_chain_attempt") \
9494
X(WALKVM_FP_CHAIN_REACHED_CODEHEAP, "walkvm_fp_chain_reached_codeheap") \
95-
X(WALKVM_ANCHOR_NOT_IN_JAVA, "walkvm_anchor_not_in_java")
95+
X(WALKVM_ANCHOR_NOT_IN_JAVA, "walkvm_anchor_not_in_java") \
96+
X(NATIVE_LIBS_DROPPED, "native_libs_dropped") \
97+
X(SIGACTION_PATCHED_LIBS, "sigaction_patched_libs") \
98+
X(SIGACTION_INTERCEPTED, "sigaction_intercepted")
9699
#define X_ENUM(a, b) a,
97100
typedef enum CounterId : int {
98101
DD_COUNTER_TABLE(X_ENUM) DD_NUM_COUNTERS

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class Libraries {
2424
// Note: Parameter is uint32_t to match lib_index packing (17 bits = max 131K libraries)
2525
CodeCache *getLibraryByIndex(uint32_t index) const {
2626
if (index < _native_libs.count()) {
27-
return _native_libs[index]; // may be NULL during concurrent add()
27+
return _native_libs[index];
2828
}
2929
return nullptr;
3030
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,19 @@ class LibraryPatcher {
2424
static int _size;
2525
static bool _patch_pthread_create;
2626

27+
// Separate tracking for sigaction patches
28+
static PatchEntry _sigaction_entries[MAX_NATIVE_LIBS];
29+
static int _sigaction_size;
30+
2731
static void patch_library_unlocked(CodeCache* lib);
2832
static void patch_pthread_create();
2933
static void patch_pthread_setspecific();
34+
static void patch_sigaction_in_library(CodeCache* lib);
3035
public:
3136
static void initialize();
3237
static void patch_libraries();
3338
static void unpatch_libraries();
39+
static void patch_sigaction();
3440
};
3541

3642
#else
@@ -40,6 +46,7 @@ class LibraryPatcher {
4046
static void initialize() { }
4147
static void patch_libraries() { }
4248
static void unpatch_libraries() { }
49+
static void patch_sigaction() { }
4350
};
4451

4552
#endif

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

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "libraryPatcher.h"
22

33
#ifdef __linux__
4+
#include "counters.h"
45
#include "profiler.h"
56
#include "vmStructs.h"
67
#include "guards.h"
@@ -17,6 +18,8 @@ SpinLock LibraryPatcher::_lock;
1718
const char* LibraryPatcher::_profiler_name = nullptr;
1819
PatchEntry LibraryPatcher::_patched_entries[MAX_NATIVE_LIBS];
1920
int LibraryPatcher::_size = 0;
21+
PatchEntry LibraryPatcher::_sigaction_entries[MAX_NATIVE_LIBS];
22+
int LibraryPatcher::_sigaction_size = 0;
2023

2124
void LibraryPatcher::initialize() {
2225
if (_profiler_name == nullptr) {
@@ -238,8 +241,59 @@ void LibraryPatcher::patch_pthread_create() {
238241
}
239242
}
240243
}
241-
#endif // __linux__
242244

245+
// Patch sigaction in all libraries to prevent any library from overwriting
246+
// our SIGSEGV/SIGBUS handlers. This protects against misbehaving libraries
247+
// (like wasmtime) that install broken signal handlers calling malloc().
248+
void LibraryPatcher::patch_sigaction_in_library(CodeCache* lib) {
249+
if (lib->name() == nullptr) return;
250+
if (_profiler_name == nullptr) return; // Not initialized yet
251+
252+
// Don't patch ourselves
253+
char path[PATH_MAX];
254+
char* resolved_path = realpath(lib->name(), path);
255+
if (resolved_path != nullptr && strcmp(resolved_path, _profiler_name) == 0) {
256+
return;
257+
}
258+
259+
// Note: We intentionally patch sanitizer libraries (libasan, libtsan, libubsan) here.
260+
// This keeps our handler on top for recoverable SIGSEGVs (e.g., safefetch) while
261+
// still chaining to the sanitizer's handler for unexpected crashes.
262+
263+
void** sigaction_location = (void**)lib->findImport(im_sigaction);
264+
if (sigaction_location == nullptr) {
265+
return;
266+
}
267+
268+
// Check if already patched or array is full
269+
if (_sigaction_size >= MAX_NATIVE_LIBS) {
270+
return;
271+
}
272+
for (int index = 0; index < _sigaction_size; index++) {
273+
if (_sigaction_entries[index]._lib == lib) {
274+
return;
275+
}
276+
}
243277

278+
void* hook = OS::getSigactionHook();
279+
_sigaction_entries[_sigaction_size]._lib = lib;
280+
_sigaction_entries[_sigaction_size]._location = sigaction_location;
281+
_sigaction_entries[_sigaction_size]._func = (void*)__atomic_load_n(sigaction_location, __ATOMIC_RELAXED);
282+
__atomic_store_n(sigaction_location, hook, __ATOMIC_RELAXED);
283+
_sigaction_size++;
284+
Counters::increment(SIGACTION_PATCHED_LIBS);
285+
}
244286

287+
void LibraryPatcher::patch_sigaction() {
288+
const CodeCacheArray& native_libs = Libraries::instance()->native_libs();
289+
int num_of_libs = native_libs.count();
290+
ExclusiveLockGuard locker(&_lock);
291+
for (int index = 0; index < num_of_libs; index++) {
292+
CodeCache* lib = native_libs.at(index);
293+
if (lib != nullptr) {
294+
patch_sigaction_in_library(lib);
295+
}
296+
}
297+
}
245298

299+
#endif // __linux__

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ class OS {
152152
static SigAction replaceSigsegvHandler(SigAction action);
153153
static SigAction replaceSigbusHandler(SigAction action);
154154

155+
// Signal handler protection - prevents other libraries from overwriting our handlers
156+
static void protectSignalHandlers(SigAction segvHandler, SigAction busHandler);
157+
static SigAction getSegvChainTarget();
158+
static SigAction getBusChainTarget();
159+
static void* getSigactionHook();
160+
155161
static int getMaxThreadId(int floor) {
156162
int maxThreadId = getMaxThreadId();
157163
return maxThreadId < floor ? floor : maxThreadId;

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

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include <time.h>
2929
#include <unistd.h>
3030
#include "common.h"
31+
#include "counters.h"
3132
#include "os.h"
3233

3334
#ifndef __musl__
@@ -726,4 +727,88 @@ SigAction OS::replaceSigbusHandler(SigAction action) {
726727
return old_action;
727728
}
728729

730+
// ============================================================================
731+
// sigaction interposition to prevent other libraries from overwriting our
732+
// SIGSEGV/SIGBUS handlers. This is needed because libraries like wasmtime
733+
// install broken signal handlers that call malloc() (not async-signal-safe).
734+
// ============================================================================
735+
736+
// Our protected handlers and their chain targets
737+
static SigAction _protected_segv_handler = nullptr;
738+
static SigAction _protected_bus_handler = nullptr;
739+
static volatile SigAction _segv_chain_target = nullptr;
740+
static volatile SigAction _bus_chain_target = nullptr;
741+
742+
// Real sigaction function pointer (resolved via dlsym)
743+
typedef int (*real_sigaction_t)(int, const struct sigaction*, struct sigaction*);
744+
static real_sigaction_t _real_sigaction = nullptr;
745+
746+
void OS::protectSignalHandlers(SigAction segvHandler, SigAction busHandler) {
747+
// Resolve real sigaction BEFORE enabling protection, while we can still use RTLD_DEFAULT
748+
if (_real_sigaction == nullptr) {
749+
_real_sigaction = (real_sigaction_t)dlsym(RTLD_DEFAULT, "sigaction");
750+
}
751+
_protected_segv_handler = segvHandler;
752+
_protected_bus_handler = busHandler;
753+
}
754+
755+
SigAction OS::getSegvChainTarget() {
756+
return __atomic_load_n(&_segv_chain_target, __ATOMIC_ACQUIRE);
757+
}
758+
759+
SigAction OS::getBusChainTarget() {
760+
return __atomic_load_n(&_bus_chain_target, __ATOMIC_ACQUIRE);
761+
}
762+
763+
// sigaction hook - called via GOT patching to intercept sigaction calls
764+
static int sigaction_hook(int signum, const struct sigaction* act, struct sigaction* oldact) {
765+
// _real_sigaction must be resolved before any GOT patching happens
766+
if (_real_sigaction == nullptr) {
767+
errno = EFAULT;
768+
return -1;
769+
}
770+
771+
// If this is SIGSEGV or SIGBUS and we have protected handlers installed,
772+
// intercept the call to keep our handler on top
773+
if (act != nullptr) {
774+
if (signum == SIGSEGV && _protected_segv_handler != nullptr) {
775+
// Only intercept SA_SIGINFO handlers (3-arg form) for safe chaining
776+
if (act->sa_flags & SA_SIGINFO) {
777+
SigAction new_handler = act->sa_sigaction;
778+
// Don't intercept if it's our own handler being installed
779+
if (new_handler != _protected_segv_handler) {
780+
// Save their handler as our chain target
781+
__atomic_exchange_n(&_segv_chain_target, new_handler, __ATOMIC_ACQ_REL);
782+
if (oldact != nullptr) {
783+
_real_sigaction(signum, nullptr, oldact);
784+
}
785+
Counters::increment(SIGACTION_INTERCEPTED);
786+
// Don't actually install their handler - keep ours on top
787+
return 0;
788+
}
789+
}
790+
// Let 1-arg handlers (without SA_SIGINFO) pass through - we can't safely chain them
791+
} else if (signum == SIGBUS && _protected_bus_handler != nullptr) {
792+
if (act->sa_flags & SA_SIGINFO) {
793+
SigAction new_handler = act->sa_sigaction;
794+
if (new_handler != _protected_bus_handler) {
795+
__atomic_exchange_n(&_bus_chain_target, new_handler, __ATOMIC_ACQ_REL);
796+
if (oldact != nullptr) {
797+
_real_sigaction(signum, nullptr, oldact);
798+
}
799+
Counters::increment(SIGACTION_INTERCEPTED);
800+
return 0;
801+
}
802+
}
803+
}
804+
}
805+
806+
// For all other cases, pass through to real sigaction
807+
return _real_sigaction(signum, act, oldact);
808+
}
809+
810+
void* OS::getSigactionHook() {
811+
return (void*)sigaction_hook;
812+
}
813+
729814
#endif // __linux__

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,4 +484,16 @@ SigAction OS::replaceSigsegvHandler(SigAction action) {
484484
return old_action;
485485
}
486486

487+
// No GOT-based sigaction interception on macOS — these are no-ops.
488+
void OS::protectSignalHandlers(SigAction segvHandler, SigAction busHandler) {
489+
}
490+
491+
SigAction OS::getSegvChainTarget() {
492+
return nullptr;
493+
}
494+
495+
SigAction OS::getBusChainTarget() {
496+
return nullptr;
497+
}
498+
487499
#endif // __APPLE__

0 commit comments

Comments
 (0)