Skip to content

Commit 57ba49e

Browse files
committed
Allocation profiling crash - Fork safety
- Use the key as synchronization - Avoid init during a get of the thread local state - Add a fork handler to ensure we have the key initialized
1 parent e8c8a83 commit 57ba49e

4 files changed

Lines changed: 66 additions & 55 deletions

File tree

include/lib/allocation_tracker.hpp

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ class AllocationTracker {
8080
// can return null (does not init)
8181
static TrackerThreadLocalState *get_tl_state();
8282

83+
// Initialize pthread key for TLS (idempotent, fork-safe)
84+
static void ensure_key_initialized();
85+
// Register pthread_atfork handler (called during init)
86+
static void register_fork_handler();
87+
8388
private:
8489
static constexpr unsigned k_ratio_max_elt_to_bitset_size = 16;
8590

@@ -120,8 +125,6 @@ class AllocationTracker {
120125

121126
static void delete_tl_state(void *tl_state);
122127

123-
static void make_key();
124-
125128
void track_allocation(uintptr_t addr, size_t size,
126129
TrackerThreadLocalState &tl_state, bool is_large_alloc);
127130
void track_deallocation(uintptr_t addr, TrackerThreadLocalState &tl_state,
@@ -160,8 +163,8 @@ class AllocationTracker {
160163

161164
// These can not be tied to the internal state of the instance.
162165
// The creation of the instance depends on this
163-
static std::atomic<int> _key_init_state;
164-
static pthread_key_t _tl_state_key;
166+
static constexpr pthread_key_t kInvalidKey = static_cast<pthread_key_t>(-1);
167+
static std::atomic<pthread_key_t> _tl_state_key;
165168

166169
static AllocationTracker *_instance;
167170
};

src/lib/allocation_tracker.cc

Lines changed: 48 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,9 @@
2828

2929
namespace ddprof {
3030

31-
namespace {
32-
// TLS key initialization states
33-
constexpr int kKeyNotInitialized = 0;
34-
constexpr int kKeyInitializing = 1;
35-
constexpr int kKeyInitialized = 2;
36-
} // namespace
37-
38-
// Static declarations
39-
std::atomic<int> AllocationTracker::_key_init_state{kKeyNotInitialized};
40-
41-
pthread_key_t AllocationTracker::_tl_state_key;
42-
43-
AllocationTracker *AllocationTracker::_instance;
31+
AllocationTracker *AllocationTracker::_instance = nullptr;
32+
std::atomic<pthread_key_t> AllocationTracker::_tl_state_key{
33+
AllocationTracker::kInvalidKey};
4434

4535
namespace {
4636
DDPROF_NOINLINE auto sleep_and_retry_reserve(MPSCRingBufferWriter &writer,
@@ -65,36 +55,15 @@ TrackerThreadLocalState *AllocationTracker::get_tl_state() {
6555
// tls_get_addr can call into malloc, which can create a recursive loop
6656
// instead we call pthread APIs to control the creation of TLS objects
6757

68-
// Thread-safe initialization using atomics (avoids pthread_once ABI issues)
69-
70-
// Fast path: relaxed load is sufficient since we only care if it's
71-
// initialized Once initialized, this value never changes, so no
72-
// synchronization needed
73-
if (_key_init_state.load(std::memory_order_relaxed) != kKeyInitialized) {
74-
// Slow path: need proper synchronization for initialization
75-
int expected = kKeyNotInitialized;
76-
if (_key_init_state.compare_exchange_strong(expected, kKeyInitializing,
77-
std::memory_order_acq_rel)) {
78-
// We won the race, do the initialization
79-
make_key();
80-
_key_init_state.store(kKeyInitialized, std::memory_order_release);
81-
} else {
82-
// Another thread is initializing or already done, wait until complete
83-
constexpr int k_max_init_wait_attempts = 100;
84-
int attempts = 0;
85-
while (_key_init_state.load(std::memory_order_acquire) !=
86-
kKeyInitialized) {
87-
if (++attempts >= k_max_init_wait_attempts) {
88-
return nullptr;
89-
}
90-
std::this_thread::yield();
91-
std::this_thread::sleep_for(std::chrono::microseconds(1));
92-
}
93-
}
94-
}
58+
// The pthread key is initialized during allocation_tracking_init() and
59+
// maintained by the fork handler, so we can directly use it here (hot path)
60+
pthread_key_t key = _tl_state_key.load(std::memory_order_relaxed);
61+
62+
// In debug builds, verify our assumption
63+
assert(key != kInvalidKey && "pthread key should be initialized before use");
9564

96-
auto *tl_state = static_cast<TrackerThreadLocalState *>(
97-
pthread_getspecific(_tl_state_key));
65+
auto *tl_state =
66+
static_cast<TrackerThreadLocalState *>(pthread_getspecific(key));
9867
return tl_state;
9968
}
10069

@@ -127,15 +96,49 @@ void AllocationTracker::delete_tl_state(void *tl_state) {
12796
delete static_cast<TrackerThreadLocalState *>(tl_state);
12897
}
12998

130-
void AllocationTracker::make_key() {
131-
// delete is called on all key objects
132-
pthread_key_create(&_tl_state_key, delete_tl_state);
99+
void AllocationTracker::ensure_key_initialized() {
100+
// Ensure pthread key is initialized (idempotent)
101+
pthread_key_t key = _tl_state_key.load(std::memory_order_acquire);
102+
103+
if (key == kInvalidKey) {
104+
pthread_key_t new_key;
105+
if (pthread_key_create(&new_key, delete_tl_state) != 0) {
106+
return; // Failed, will be retried later
107+
}
108+
109+
pthread_key_t expected = kInvalidKey;
110+
if (!_tl_state_key.compare_exchange_strong(expected, new_key,
111+
std::memory_order_release)) {
112+
// Another thread beat us, clean up our key
113+
pthread_key_delete(new_key);
114+
}
115+
}
116+
}
117+
118+
static void child_after_fork_handler() {
119+
// After fork in child, verify the pthread key is still valid.
120+
// The key itself survives fork, but we want to ensure it's properly set.
121+
AllocationTracker::ensure_key_initialized();
122+
}
123+
124+
void AllocationTracker::register_fork_handler() {
125+
static bool registered = false;
126+
if (!registered) {
127+
pthread_atfork(nullptr, nullptr, child_after_fork_handler);
128+
registered = true;
129+
}
133130
}
134131

135132
DDRes AllocationTracker::allocation_tracking_init(
136133
uint64_t allocation_profiling_rate, uint32_t flags,
137134
uint32_t stack_sample_size, const RingBufferInfo &ring_buffer,
138135
const IntervalTimerCheck &timer_check) {
136+
// Register fork handler to ensure key is valid after fork
137+
register_fork_handler();
138+
139+
// Ensure pthread key is initialized before we try to use it
140+
ensure_key_initialized();
141+
139142
TrackerThreadLocalState *tl_state = get_tl_state();
140143
if (!tl_state) {
141144
// This is the time at which the init_tl_state should not fail

test/allocation_tracker_fork_test.c

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ extern "C" {
2020

2121
void *AllocationTracker_get_tl_state(void);
2222
void *AllocationTracker_init_tl_state(void);
23+
void AllocationTracker_ensure_key_initialized(void);
2324

2425
#ifdef __cplusplus
2526
}
@@ -43,9 +44,10 @@ static void *thread_test_func(void *arg) {
4344
}
4445

4546
int main(void) {
46-
// Initialize pthread key and create TLS data in parent
47-
void *state_before = AllocationTracker_get_tl_state();
47+
// Initialize pthread key before using it
48+
AllocationTracker_ensure_key_initialized();
4849

50+
// Initialize pthread key and create TLS data in parent
4951
void *parent_state = AllocationTracker_init_tl_state();
5052
if (parent_state == NULL) {
5153
fprintf(stderr, "FAIL: Parent init_tl_state() returned NULL\n");
@@ -70,8 +72,6 @@ int main(void) {
7072

7173
if (pid == 0) {
7274
// Child process - test pthread key works after fork
73-
void *child_inherited = AllocationTracker_get_tl_state();
74-
7575
void *child_state = AllocationTracker_init_tl_state();
7676
if (child_state == NULL) {
7777
fprintf(stderr, "FAIL: Child init_tl_state() returned NULL\n");
@@ -94,6 +94,7 @@ int main(void) {
9494

9595
pthread_join(thread, NULL);
9696
if (thread_result != 0) {
97+
fprintf(stderr, "FAIL: Thread test failed - new thread TLS not NULL\n");
9798
_exit(4);
9899
}
99100

@@ -121,11 +122,11 @@ int main(void) {
121122
if (exit_code == 1)
122123
reason = "init_tl_state failed";
123124
else if (exit_code == 2)
124-
reason = "pthread key broken";
125+
reason = "pthread key broken after fork";
125126
else if (exit_code == 3)
126127
reason = "pthread_create failed";
127128
else if (exit_code == 4)
128-
reason = "thread test failed";
129+
reason = "new thread TLS not NULL";
129130
fprintf(stderr, "FAIL: Child exited with code %d (%s)\n", exit_code,
130131
reason);
131132
return 1;

test/allocation_tracker_fork_test_wrapper.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ void *AllocationTracker_get_tl_state(void) {
1313
return ddprof::AllocationTracker::get_tl_state();
1414
}
1515

16+
void AllocationTracker_ensure_key_initialized(void) {
17+
ddprof::AllocationTracker::ensure_key_initialized();
18+
}
19+
1620
void *AllocationTracker_init_tl_state(void) {
1721
return ddprof::AllocationTracker::init_tl_state();
1822
}

0 commit comments

Comments
 (0)