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
7679static 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
117123static 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+
134162void
135163sentrycrashbic_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
163192sentrycrashbic_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
218248void
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
247279void
248280sentrycrashbic_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
258300void
259301sentrycrashbic_registerRemovedCallback (sentrycrashbic_cacheChangeCallback callback )
260302{
261- imageRemovedCallback = callback ;
303+ atomic_store_explicit ( & g_removedCallback , callback , memory_order_release ) ;
262304}
0 commit comments