Skip to content

Commit e2fb3e7

Browse files
committed
fix: Fill dyld image cache asynchronously to avoid increasing app launch time
sentrycrashbic_startCache now dispatches its work on a background queue so that the expensive _dyld_register_func_for_add_image callback (which calls dladdr for every loaded image) does not block the main thread during SDK initialization. Since the cache may not be populated yet when SentryUIViewControllerSwizzling.start() runs, replace its SentryBinaryImageCache lookup with direct synchronous iteration of _dyld_image_count/_dyld_get_image_name. Performance-sensitive users can disable UIViewController swizzling. Also fix a race condition in SentryCrashBinaryImageCache where stopCache resets g_next_index while a concurrent add_dyld_image could increment it afterwards. Move the reset into startCacheImpl and gate iterateOverImages/registerAddedCallback on g_started.
1 parent 3a42337 commit e2fb3e7

10 files changed

Lines changed: 209 additions & 236 deletions

File tree

Lines changed: 156 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#include "SentryCrashBinaryImageCache.h"
22
#include "SentryCrashDynamicLinker.h"
3+
#include <dispatch/dispatch.h>
34
#include <mach-o/dyld.h>
45
#include <mach-o/dyld_images.h>
56
#include <pthread.h>
7+
#include <stdatomic.h>
68
#include <stdio.h>
79
#include <stdlib.h>
810
#include <string.h>
@@ -60,94 +62,121 @@ sentry_resetFuncForAddRemoveImage(void)
6062
# define _will_add_image()
6163
#endif // defined(SENTRY_TEST) || defined(SENTRY_TEST_CI) || defined(DEBUG)
6264

63-
typedef struct SentryCrashBinaryImageNode {
65+
#define MAX_DYLD_IMAGES 4096
66+
67+
typedef struct {
68+
_Atomic(uint32_t) ready; // 0 = not published, 1 = published
6469
SentryCrashBinaryImage image;
65-
bool available;
66-
struct SentryCrashBinaryImageNode *next;
67-
} SentryCrashBinaryImageNode;
70+
} PublishedBinaryImage;
6871

69-
static SentryCrashBinaryImageNode rootNode = { 0 };
70-
static SentryCrashBinaryImageNode *tailNode = NULL;
71-
static pthread_mutex_t binaryImagesMutex = PTHREAD_MUTEX_INITIALIZER;
72+
static PublishedBinaryImage g_images[MAX_DYLD_IMAGES];
73+
static _Atomic(uint32_t) g_next_index = 0;
74+
static _Atomic(uint32_t) g_started = 0;
7275

73-
static sentrycrashbic_cacheChangeCallback imageAddedCallback = NULL;
74-
static sentrycrashbic_cacheChangeCallback imageRemovedCallback = NULL;
76+
static _Atomic(sentrycrashbic_cacheChangeCallback) g_addedCallback = NULL;
77+
static _Atomic(sentrycrashbic_cacheChangeCallback) g_removedCallback = NULL;
7578

7679
static void
77-
binaryImageAdded(const struct mach_header *header, intptr_t slide)
80+
add_dyld_image(const struct mach_header *mh)
7881
{
79-
pthread_mutex_lock(&binaryImagesMutex);
80-
if (tailNode == NULL) {
81-
pthread_mutex_unlock(&binaryImagesMutex);
82+
// Don't add images if the cache is not started
83+
if (!atomic_load_explicit(&g_started, memory_order_acquire)) {
8284
return;
8385
}
84-
pthread_mutex_unlock(&binaryImagesMutex);
86+
87+
// Check dladdr first, before reserving a slot in the array.
88+
// If we increment g_next_index before this check and dladdr fails,
89+
// we'd create a "hole" with ready=0 that stops iteration.
8590
Dl_info info;
86-
if (!dladdr(header, &info) || info.dli_fname == NULL) {
91+
if (!dladdr(mh, &info) || info.dli_fname == NULL) {
8792
return;
8893
}
8994

90-
SentryCrashBinaryImage binaryImage = { 0 };
91-
if (!sentrycrashdl_getBinaryImageForHeader(
92-
(const void *)header, info.dli_fname, &binaryImage, false)) {
95+
// Test hook: called just before adding the image
96+
_will_add_image();
97+
98+
uint32_t idx = atomic_fetch_add_explicit(&g_next_index, 1, memory_order_relaxed);
99+
100+
if (idx >= MAX_DYLD_IMAGES) {
93101
return;
94102
}
95103

96-
SentryCrashBinaryImageNode *newNode = malloc(sizeof(SentryCrashBinaryImageNode));
97-
newNode->available = true;
98-
newNode->image = binaryImage;
99-
newNode->next = NULL;
100-
_will_add_image();
101-
pthread_mutex_lock(&binaryImagesMutex);
102-
// Recheck tailNode as it could be null when
103-
// stopped from another thread.
104-
if (tailNode != NULL) {
105-
tailNode->next = newNode;
106-
tailNode = tailNode->next;
107-
} else {
108-
free(newNode);
109-
newNode = NULL;
110-
}
111-
pthread_mutex_unlock(&binaryImagesMutex);
112-
if (newNode && imageAddedCallback) {
113-
imageAddedCallback(&newNode->image);
104+
PublishedBinaryImage *entry = &g_images[idx];
105+
sentrycrashdl_getBinaryImageForHeader(mh, info.dli_fname, &entry->image, false);
106+
107+
// Read callback BEFORE publishing to avoid race with registerAddedCallback.
108+
// If callback is NULL here, the registering thread will see ready=1 and call it.
109+
// If callback is non-NULL here, we call it and the registering thread will either
110+
// not have started iterating yet, or will skip this image since it wasn't ready
111+
// when it read g_next_index.
112+
sentrycrashbic_cacheChangeCallback callback
113+
= atomic_load_explicit(&g_addedCallback, memory_order_acquire);
114+
115+
// ---- Publish ----
116+
atomic_store_explicit(&entry->ready, 1, memory_order_release);
117+
118+
if (callback != NULL) {
119+
callback(&entry->image);
114120
}
115121
}
116122

117123
static void
118-
binaryImageRemoved(const struct mach_header *header, intptr_t slide)
124+
dyld_add_image_cb(const struct mach_header *mh, intptr_t slide)
119125
{
120-
SentryCrashBinaryImageNode *nextNode = &rootNode;
126+
add_dyld_image(mh);
127+
}
121128

122-
while (nextNode != NULL) {
123-
if (nextNode->image.address == (uint64_t)header) {
124-
nextNode->available = false;
125-
if (imageRemovedCallback) {
126-
imageRemovedCallback(&nextNode->image);
129+
static void
130+
dyld_remove_image_cb(const struct mach_header *mh, intptr_t slide)
131+
{
132+
sentrycrashbic_cacheChangeCallback callback
133+
= atomic_load_explicit(&g_removedCallback, memory_order_acquire);
134+
135+
// Find the image in our cache by matching the header address
136+
uint32_t count = atomic_load_explicit(&g_next_index, memory_order_acquire);
137+
if (count > MAX_DYLD_IMAGES)
138+
count = MAX_DYLD_IMAGES;
139+
140+
for (uint32_t i = 0; i < count; i++) {
141+
PublishedBinaryImage *src = &g_images[i];
142+
if (!atomic_load_explicit(&src->ready, memory_order_acquire)) {
143+
continue;
144+
}
145+
if (src->image.address == (uintptr_t)mh) {
146+
atomic_store_explicit(&src->ready, 0, memory_order_release);
147+
if (callback) {
148+
callback(&src->image);
149+
return;
127150
}
128-
break;
129151
}
130-
nextNode = nextNode->next;
131152
}
132153
}
133154

155+
static void
156+
dyld_tracker_start(void)
157+
{
158+
sentry_dyld_register_func_for_add_image(dyld_add_image_cb);
159+
sentry_dyld_register_func_for_remove_image(dyld_remove_image_cb);
160+
}
161+
134162
void
135163
sentrycrashbic_iterateOverImages(sentrycrashbic_imageIteratorCallback callback, void *context)
136164
{
137-
/**
138-
We can't use locks here because this is meant to be used during crashes,
139-
where we can't use async unsafe functions. In order to avoid potential problems,
140-
we choose an approach that doesn't remove nodes from the list.
141-
*/
142-
SentryCrashBinaryImageNode *nextNode = &rootNode;
143-
144-
// If tailNode is null it means the cache was stopped, therefore we end the iteration.
145-
// This will minimize any race condition effect without the need for locks.
146-
while (nextNode != NULL && tailNode != NULL) {
147-
if (nextNode->available) {
148-
callback(&nextNode->image, context);
165+
if (!atomic_load_explicit(&g_started, memory_order_acquire)) {
166+
return;
167+
}
168+
169+
uint32_t count = atomic_load_explicit(&g_next_index, memory_order_acquire);
170+
171+
if (count > MAX_DYLD_IMAGES)
172+
count = MAX_DYLD_IMAGES;
173+
174+
for (uint32_t i = 0; i < count; i++) {
175+
PublishedBinaryImage *src = &g_images[i];
176+
177+
if (atomic_load_explicit(&src->ready, memory_order_acquire)) {
178+
callback(&src->image, context);
149179
}
150-
nextNode = nextNode->next;
151180
}
152181
}
153182

@@ -159,7 +188,7 @@ sentrycrashbic_iterateOverImages(sentrycrashbic_imageIteratorCallback callback,
159188
* @return true if dyld is not found in the loaded images and should be added to the cache,
160189
* false if dyld is already present in the loaded images.
161190
*/
162-
bool
191+
static bool
163192
sentrycrashbic_shouldAddDyld(void)
164193
{
165194
// dyld is different from libdyld.dylib; the latter contains the public API
@@ -170,93 +199,106 @@ sentrycrashbic_shouldAddDyld(void)
170199

171200
// Since Apple no longer includes dyld in the images listed `_dyld_image_count` and related
172201
// functions We manually include it to our cache.
173-
SentryCrashBinaryImageNode *
174-
sentrycrashbic_getDyldNode(void)
202+
// Note: This bypasses add_dyld_image() because dladdr() returns NULL for dyld, so we need
203+
// to use sentrycrashdl_getBinaryImageForHeader() directly with a hardcoded filename.
204+
static void
205+
sentrycrashbic_addDyldNode(void)
175206
{
176207
const struct mach_header *header = sentryDyldHeader;
177208

178-
SentryCrashBinaryImage binaryImage = { 0 };
179-
if (!sentrycrashdl_getBinaryImageForHeader((const void *)header, "dyld", &binaryImage, false)) {
180-
return NULL;
209+
uint32_t idx = atomic_fetch_add_explicit(&g_next_index, 1, memory_order_relaxed);
210+
if (idx >= MAX_DYLD_IMAGES) {
211+
return;
181212
}
182213

183-
SentryCrashBinaryImageNode *newNode = malloc(sizeof(SentryCrashBinaryImageNode));
184-
newNode->available = true;
185-
newNode->image = binaryImage;
186-
newNode->next = NULL;
214+
PublishedBinaryImage *entry = &g_images[idx];
215+
if (!sentrycrashdl_getBinaryImageForHeader(
216+
(const void *)header, "dyld", &entry->image, false)) {
217+
// Decrement because we couldn't add the image
218+
atomic_fetch_sub_explicit(&g_next_index, 1, memory_order_relaxed);
219+
return;
220+
}
187221

188-
return newNode;
222+
atomic_store_explicit(&entry->ready, 1, memory_order_release);
189223
}
190224

191-
void
192-
sentrycrashbic_startCache(void)
225+
static void
226+
sentrycrashbic_startCacheImpl(void)
193227
{
194-
pthread_mutex_lock(&binaryImagesMutex);
195-
if (tailNode != NULL) {
196-
// Already initialized
197-
pthread_mutex_unlock(&binaryImagesMutex);
228+
// Check if already started
229+
uint32_t expected = 0;
230+
if (!atomic_compare_exchange_strong_explicit(
231+
&g_started, &expected, 1, memory_order_acq_rel, memory_order_relaxed)) {
198232
return;
199233
}
200234

235+
// Reset g_next_index here rather than in stopCache to avoid the race where
236+
// a concurrent add_dyld_image increments g_next_index after stopCache resets it.
237+
// The compare-exchange above guarantees we are the only thread in this function.
238+
atomic_store_explicit(&g_next_index, 0, memory_order_release);
239+
201240
if (sentrycrashbic_shouldAddDyld()) {
202241
sentrycrashdl_initialize();
203-
SentryCrashBinaryImageNode *dyldNode = sentrycrashbic_getDyldNode();
204-
tailNode = dyldNode;
205-
rootNode.next = dyldNode;
206-
} else {
207-
tailNode = &rootNode;
208-
rootNode.next = NULL;
242+
sentrycrashbic_addDyldNode();
209243
}
210-
pthread_mutex_unlock(&binaryImagesMutex);
211244

212-
// During a call to _dyld_register_func_for_add_image() the callback func is called for every
213-
// existing image
214-
sentry_dyld_register_func_for_add_image(&binaryImageAdded);
215-
sentry_dyld_register_func_for_remove_image(&binaryImageRemoved);
245+
dyld_tracker_start();
216246
}
217247

218248
void
219-
sentrycrashbic_stopCache(void)
249+
sentrycrashbic_startCache(void)
220250
{
221-
pthread_mutex_lock(&binaryImagesMutex);
222-
if (tailNode == NULL) {
223-
pthread_mutex_unlock(&binaryImagesMutex);
224-
return;
225-
}
226-
227-
SentryCrashBinaryImageNode *node = rootNode.next;
228-
rootNode.next = NULL;
229-
tailNode = NULL;
230-
231-
while (node != NULL) {
232-
SentryCrashBinaryImageNode *nextNode = node->next;
233-
free(node);
234-
node = nextNode;
235-
}
236-
237-
pthread_mutex_unlock(&binaryImagesMutex);
251+
// During a call to _dyld_register_func_for_add_image() the callback func is called for every
252+
// existing image
253+
// This must be done on a background thread to not block app launch due to the extensive use of
254+
// locks in the image added callback. The main culprit is the calls to `dladdr`. The downside of
255+
// doing this async is if there is a crash very shortly after app launch we might not have
256+
// recorded all the load addresses of images yet. We think this is an acceptible tradeoff to not
257+
// block app launch, since it's always possible to crash early in app launch before Sentry can
258+
// capture the crash.
259+
#if defined(SENTRY_TEST) || defined(SENTRY_TEST_CI)
260+
sentrycrashbic_startCacheImpl();
261+
#else
262+
dispatch_async(
263+
dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ sentrycrashbic_startCacheImpl(); });
264+
#endif
238265
}
239266

240-
static void
241-
initialReportToCallback(SentryCrashBinaryImage *image, void *context)
267+
void
268+
sentrycrashbic_stopCache(void)
242269
{
243-
sentrycrashbic_cacheChangeCallback callback = (sentrycrashbic_cacheChangeCallback)context;
244-
callback(image);
270+
// Only flip the started flag. We intentionally do NOT reset g_next_index here
271+
// because a concurrent add_dyld_image (that already passed the g_started check)
272+
// could increment g_next_index after our reset, leaving the cache in an
273+
// inconsistent state. Instead, g_next_index is reset in startCacheImpl where
274+
// we have exclusive access via the compare-exchange. iterateOverImages checks
275+
// g_started before reading g_next_index, so the cache appears empty immediately.
276+
atomic_store_explicit(&g_started, 0, memory_order_release);
245277
}
246278

247279
void
248280
sentrycrashbic_registerAddedCallback(sentrycrashbic_cacheChangeCallback callback)
249281
{
250-
imageAddedCallback = callback;
251-
if (callback) {
252-
pthread_mutex_lock(&binaryImagesMutex);
253-
sentrycrashbic_iterateOverImages(&initialReportToCallback, callback);
254-
pthread_mutex_unlock(&binaryImagesMutex);
282+
atomic_store_explicit(&g_addedCallback, callback, memory_order_release);
283+
284+
if (callback != NULL && atomic_load_explicit(&g_started, memory_order_acquire)) {
285+
// Call for all existing images already in the cache
286+
uint32_t count = atomic_load_explicit(&g_next_index, memory_order_acquire);
287+
if (count > MAX_DYLD_IMAGES)
288+
count = MAX_DYLD_IMAGES;
289+
290+
for (uint32_t i = 0; i < count; i++) {
291+
PublishedBinaryImage *src = &g_images[i];
292+
if (!atomic_load_explicit(&src->ready, memory_order_acquire)) {
293+
break;
294+
}
295+
callback(&src->image);
296+
}
255297
}
256298
}
257299

258300
void
259301
sentrycrashbic_registerRemovedCallback(sentrycrashbic_cacheChangeCallback callback)
260302
{
261-
imageRemovedCallback = callback;
303+
atomic_store_explicit(&g_removedCallback, callback, memory_order_release);
262304
}

0 commit comments

Comments
 (0)