Skip to content

Commit 4a21751

Browse files
committed
WIP
1 parent 14e6e39 commit 4a21751

File tree

8 files changed

+174
-95
lines changed

8 files changed

+174
-95
lines changed

Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.c

Lines changed: 148 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +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>
67
#include <stdio.h>
7-
#include <dispatch/dispatch.h>
88
#include <stdlib.h>
99
#include <string.h>
1010
#include <unistd.h>
@@ -66,59 +66,116 @@ sentry_resetFuncForAddRemoveImage(void)
6666
#define MAX_DYLD_IMAGES 4096
6767

6868
typedef struct {
69-
_Atomic(uint32_t) ready; // 0 = not published, 1 = published
70-
SentryCrashBinaryImage image;
69+
_Atomic(uint32_t) ready; // 0 = not published, 1 = published
70+
SentryCrashBinaryImage image;
7171
} PublishedBinaryImage;
7272

7373
static PublishedBinaryImage g_images[MAX_DYLD_IMAGES];
7474
static _Atomic(uint32_t) g_next_index = 0;
7575
static _Atomic(uint32_t) g_overflowed = 0;
76+
static _Atomic(uint32_t) g_started = 0;
7677

77-
static void add_dyld_image(const struct mach_header* mh) {
78-
uint32_t idx =
79-
atomic_fetch_add_explicit(&g_next_index, 1, memory_order_relaxed);
78+
static _Atomic(sentrycrashbic_cacheChangeCallback) g_addedCallback = NULL;
79+
static _Atomic(sentrycrashbic_cacheChangeCallback) g_removedCallback = NULL;
8080

81-
if (idx >= MAX_DYLD_IMAGES) {
82-
atomic_store_explicit(&g_overflowed, 1, memory_order_relaxed);
81+
static void
82+
add_dyld_image(const struct mach_header *mh)
83+
{
84+
// Don't add images if the cache is not started
85+
if (!atomic_load_explicit(&g_started, memory_order_acquire)) {
8386
return;
8487
}
85-
88+
89+
// Check dladdr first, before reserving a slot in the array.
90+
// If we increment g_next_index before this check and dladdr fails,
91+
// we'd create a "hole" with ready=0 that stops iteration.
8692
Dl_info info;
8793
if (!dladdr(mh, &info) || info.dli_fname == NULL) {
8894
return;
8995
}
9096

91-
PublishedBinaryImage* entry = &g_images[idx];
97+
// Test hook: called just before adding the image
98+
_will_add_image();
99+
100+
uint32_t idx = atomic_fetch_add_explicit(&g_next_index, 1, memory_order_relaxed);
101+
102+
if (idx >= MAX_DYLD_IMAGES) {
103+
atomic_store_explicit(&g_overflowed, 1, memory_order_relaxed);
104+
return;
105+
}
106+
107+
PublishedBinaryImage *entry = &g_images[idx];
92108
sentrycrashdl_getBinaryImageForHeader(mh, info.dli_fname, &entry->image, false);
93109

110+
// Read callback BEFORE publishing to avoid race with registerAddedCallback.
111+
// If callback is NULL here, the registering thread will see ready=1 and call it.
112+
// If callback is non-NULL here, we call it and the registering thread will either
113+
// not have started iterating yet, or will skip this image since it wasn't ready
114+
// when it read g_next_index.
115+
sentrycrashbic_cacheChangeCallback callback
116+
= atomic_load_explicit(&g_addedCallback, memory_order_acquire);
117+
94118
// ---- Publish ----
95119
atomic_store_explicit(&entry->ready, 1, memory_order_release);
120+
121+
if (callback != NULL) {
122+
callback(&entry->image);
123+
}
96124
}
97125

98-
static void dyld_add_image_cb(const struct mach_header* mh, intptr_t slide) {
126+
static void
127+
dyld_add_image_cb(const struct mach_header *mh, intptr_t slide)
128+
{
99129
add_dyld_image(mh);
100130
}
101131

102-
void dyld_tracker_start(void) {
103-
sentry_dyld_register_func_for_add_image(dyld_add_image_cb);
132+
static void
133+
dyld_remove_image_cb(const struct mach_header *mh, intptr_t slide)
134+
{
135+
sentrycrashbic_cacheChangeCallback callback
136+
= atomic_load_explicit(&g_removedCallback, memory_order_acquire);
137+
138+
// Find the image in our cache by matching the header address
139+
uint32_t count = atomic_load_explicit(&g_next_index, memory_order_acquire);
140+
if (count > MAX_DYLD_IMAGES)
141+
count = MAX_DYLD_IMAGES;
142+
143+
for (uint32_t i = 0; i < count; i++) {
144+
PublishedBinaryImage *src = &g_images[i];
145+
if (!atomic_load_explicit(&src->ready, memory_order_acquire)) {
146+
continue;
147+
}
148+
if (src->image.address == (uintptr_t)mh) {
149+
atomic_store_explicit(&src->ready, 0, memory_order_release);
150+
if (callback) {
151+
callback(&src->image);
152+
return;
153+
}
154+
}
155+
}
156+
}
157+
158+
static void
159+
dyld_tracker_start(void)
160+
{
161+
sentry_dyld_register_func_for_add_image(dyld_add_image_cb);
162+
sentry_dyld_register_func_for_remove_image(dyld_remove_image_cb);
104163
}
105164

106165
void
107166
sentrycrashbic_iterateOverImages(sentrycrashbic_imageIteratorCallback callback, void *context)
108167
{
109-
uint32_t count =
110-
atomic_load_explicit(&g_next_index, memory_order_acquire);
168+
uint32_t count = atomic_load_explicit(&g_next_index, memory_order_acquire);
111169

112-
if (count > MAX_DYLD_IMAGES) count = MAX_DYLD_IMAGES;
170+
if (count > MAX_DYLD_IMAGES)
171+
count = MAX_DYLD_IMAGES;
113172

114173
for (uint32_t i = 0; i < count; i++) {
115-
PublishedBinaryImage* src = &g_images[i];
174+
PublishedBinaryImage *src = &g_images[i];
116175

117-
if (!atomic_load_explicit(&src->ready, memory_order_acquire)) {
118-
return; // stop at first unpublished entry
176+
if (atomic_load_explicit(&src->ready, memory_order_acquire)) {
177+
callback(&src->image, context);
119178
}
120-
121-
callback(&src->image, context);
122179
}
123180
}
124181

@@ -130,7 +187,7 @@ sentrycrashbic_iterateOverImages(sentrycrashbic_imageIteratorCallback callback,
130187
* @return true if dyld is not found in the loaded images and should be added to the cache,
131188
* false if dyld is already present in the loaded images.
132189
*/
133-
bool
190+
static bool
134191
sentrycrashbic_shouldAddDyld(void)
135192
{
136193
// dyld is different from libdyld.dylib; the latter contains the public API
@@ -141,57 +198,104 @@ sentrycrashbic_shouldAddDyld(void)
141198

142199
// Since Apple no longer includes dyld in the images listed `_dyld_image_count` and related
143200
// functions We manually include it to our cache.
144-
void
201+
// Note: This bypasses add_dyld_image() because dladdr() returns NULL for dyld, so we need
202+
// to use sentrycrashdl_getBinaryImageForHeader() directly with a hardcoded filename.
203+
static void
145204
sentrycrashbic_addDyldNode(void)
146205
{
147206
const struct mach_header *header = sentryDyldHeader;
148207

149-
SentryCrashBinaryImage binaryImage = { 0 };
150-
if (!sentrycrashdl_getBinaryImageForHeader((const void *)header, "dyld", &binaryImage, false)) {
208+
uint32_t idx = atomic_fetch_add_explicit(&g_next_index, 1, memory_order_relaxed);
209+
if (idx >= MAX_DYLD_IMAGES) {
210+
atomic_store_explicit(&g_overflowed, 1, memory_order_relaxed);
151211
return;
152212
}
153-
add_dyld_image(header);
213+
214+
PublishedBinaryImage *entry = &g_images[idx];
215+
if (!sentrycrashdl_getBinaryImageForHeader((const void *)header, "dyld", &entry->image, false)) {
216+
// Decrement because we couldn't add the image
217+
atomic_fetch_sub_explicit(&g_next_index, 1, memory_order_relaxed);
218+
return;
219+
}
220+
221+
sentrycrashbic_cacheChangeCallback callback
222+
= atomic_load_explicit(&g_addedCallback, memory_order_acquire);
223+
atomic_store_explicit(&entry->ready, 1, memory_order_release);
224+
if (callback != NULL) {
225+
callback(&entry->image);
226+
}
154227
}
155228

156-
void
157-
sentrycrashbic_startCache(void)
229+
static void
230+
sentrycrashbic_startCacheImpl(void)
158231
{
232+
// Check if already started
233+
uint32_t expected = 0;
234+
if (!atomic_compare_exchange_strong_explicit(
235+
&g_started, &expected, 1, memory_order_acq_rel, memory_order_relaxed)) {
236+
return;
237+
}
238+
159239
if (sentrycrashbic_shouldAddDyld()) {
160240
sentrycrashdl_initialize();
161241
sentrycrashbic_addDyldNode();
162242
}
163243

244+
dyld_tracker_start();
245+
}
246+
247+
void
248+
sentrycrashbic_startCache(void)
249+
{
164250
// During a call to _dyld_register_func_for_add_image() the callback func is called for every
165251
// existing image
166-
// This must be done on a background thread to not block app launch due to the extensive use of locks
167-
// in the image added callback. The main culprit is the calls to `dladdr`.
168-
// The downside of doing this async is if there is a crash very shortly after app launch we might
169-
// not have recorded all the load addresses of images yet. We think this is an acceptible tradeoff
170-
// to not block app launch, since it's always possible to crash early in app launch before
171-
// Sentry can capture the crash.
172-
//
173-
// A future update to this code could record everything but the filename synchronously, and then
174-
// add in the image name async. However, that would still block app launch somewhat and it's not
175-
// clear that is worth the benefits.
176-
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
177-
dyld_tracker_start();
178-
});
252+
// This must be done on a background thread to not block app launch due to the extensive use of
253+
// locks in the image added callback. The main culprit is the calls to `dladdr`. The downside of
254+
// doing this async is if there is a crash very shortly after app launch we might not have
255+
// recorded all the load addresses of images yet. We think this is an acceptible tradeoff to not
256+
// block app launch, since it's always possible to crash early in app launch before Sentry can
257+
// capture the crash.
258+
#if defined(SENTRY_TEST) || defined(SENTRY_TEST_CI)
259+
sentrycrashbic_startCacheImpl();
260+
#else
261+
dispatch_async(
262+
dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ sentrycrashbic_startCacheImpl(); });
263+
#endif
179264
}
180265

181266
void
182267
sentrycrashbic_stopCache(void)
183268
{
184-
// TODO: Why do we need to support stopping it?
269+
// The binary image cache does not allocate memory, the cache takes a fixed
270+
// size, it's not malloced. So this is kind of pointless, but there are a bunch
271+
// of tests using it. We should clean it up and make startCache use dispatchOnce
272+
atomic_store_explicit(&g_started, 0, memory_order_release);
273+
atomic_store_explicit(&g_next_index, 0, memory_order_release);
185274
}
186275

187276
void
188277
sentrycrashbic_registerAddedCallback(sentrycrashbic_cacheChangeCallback callback)
189278
{
190-
// TODO: I think this is only for when we used to have on-device symbolication. Should be able to remove now
279+
atomic_store_explicit(&g_addedCallback, callback, memory_order_release);
280+
281+
if (callback != NULL) {
282+
// Call for all existing images already in the cache
283+
uint32_t count = atomic_load_explicit(&g_next_index, memory_order_acquire);
284+
if (count > MAX_DYLD_IMAGES)
285+
count = MAX_DYLD_IMAGES;
286+
287+
for (uint32_t i = 0; i < count; i++) {
288+
PublishedBinaryImage *src = &g_images[i];
289+
if (!atomic_load_explicit(&src->ready, memory_order_acquire)) {
290+
break;
291+
}
292+
callback(&src->image);
293+
}
294+
}
191295
}
192296

193297
void
194298
sentrycrashbic_registerRemovedCallback(sentrycrashbic_cacheChangeCallback callback)
195299
{
196-
// TODO: I think this is only for when we used to have on-device symbolication. Should be able to remove now
300+
atomic_store_explicit(&g_removedCallback, callback, memory_order_release);
197301
}

Sources/Swift/Core/Helper/SentryBinaryImageCache.swift

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ import Foundation
4242
return
4343
}
4444
let image = imagePtr.pointee
45-
SentryDependencyContainer.sharedInstance().binaryImageCache.binaryImageAdded(
46-
imageName: image.name, vmAddress: image.vmAddress, address: image.address, size: image.size, uuid: image.uuid)
45+
SentryDependencyContainer.sharedInstance().binaryImageCache.binaryImageAdded(image: image)
4746
}
4847
sentrycrashbic_registerRemovedCallback { imagePtr in
4948
guard let imagePtr else {
@@ -64,14 +63,8 @@ import Foundation
6463
}
6564
}
6665

67-
// We have to expand `SentryCrashBinaryImage` since the model is defined in SentryPrivate
68-
@objc(binaryImageAdded:vmAddress:address:size:uuid:)
69-
public func binaryImageAdded(imageName: UnsafePointer<CChar>?,
70-
vmAddress: UInt64,
71-
address: UInt64,
72-
size: UInt64,
73-
uuid: UnsafePointer<UInt8>?) {
74-
guard let imageName else {
66+
func binaryImageAdded(image: SentryCrashBinaryImage) {
67+
guard let imageName = image.name else {
7568
SentrySDKLog.warning("The image name was NULL. Can't add image to cache.")
7669
return
7770
}
@@ -82,10 +75,10 @@ import Foundation
8275

8376
let newImage = SentryBinaryImageInfo(
8477
name: nameString,
85-
uuid: Self.convertUUID(uuid),
86-
vmAddress: vmAddress,
87-
address: address,
88-
size: size
78+
uuid: Self.convertUUID(image.uuid),
79+
vmAddress: image.vmAddress,
80+
address: image.address,
81+
size: image.size
8982
)
9083

9184
lock.synchronized {
@@ -118,18 +111,16 @@ import Foundation
118111
dispatchQueueWrapper: Dependencies.dispatchQueueWrapper)
119112
}
120113
}
121-
122-
@objc
123-
public static func convertUUID(_ value: UnsafePointer<UInt8>?) -> String? {
114+
115+
private static func convertUUID(_ value: UnsafePointer<UInt8>?) -> String? {
124116
guard let value = value else { return nil }
125117

126118
var uuidBuffer = [CChar](repeating: 0, count: 37)
127119
sentrycrashdl_convertBinaryImageUUID(value, &uuidBuffer)
128120
return String(cString: uuidBuffer, encoding: .ascii)
129121
}
130-
131-
@objc
132-
public func binaryImageRemoved(_ imageAddress: UInt64) {
122+
123+
func binaryImageRemoved(_ imageAddress: UInt64) {
133124
lock.synchronized {
134125
guard let index = indexOfImage(address: imageAddress) else { return }
135126
self.cache?.remove(at: index)
@@ -166,8 +157,7 @@ import Foundation
166157
return nil
167158
}
168159

169-
@objc(imagePathsForInAppInclude:)
170-
public func imagePathsFor(inAppInclude: String) -> Set<String> {
160+
func imagePathsFor(inAppInclude: String) -> Set<String> {
171161
lock.synchronized {
172162
var imagePaths = Set<String>()
173163

@@ -182,8 +172,7 @@ import Foundation
182172
}
183173
}
184174

185-
@objc
186-
public func getAllBinaryImages() -> [SentryBinaryImageInfo] {
175+
func getAllBinaryImages() -> [SentryBinaryImageInfo] {
187176
lock.synchronized {
188177
return cache ?? []
189178
}

Sources/Swift/SentryCrash/SentryDebugImageProvider.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// swiftlint:disable missing_docs
22
@_implementationOnly import _SentryPrivate
33

4+
// Wraps `SentryBinaryImageCache` which is not reentrant. It's a faster way to get an image name from an address
5+
// but cannot be used from async signal safe code.
46
@_spi(Private) @objc public class SentryDebugImageProvider: NSObject {
57

68
private static let debugImageType = "macho"

Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerSwizzlingTests.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,7 @@ class SentryUIViewControllerSwizzlingTests: XCTestCase {
200200

201201
let image = createCrashBinaryImage(0, name: debugDylib)
202202
SentryDependencyContainer.sharedInstance().binaryImageCache.start(false)
203-
SentryDependencyContainer.sharedInstance().binaryImageCache.binaryImageAdded(imageName: image.name,
204-
vmAddress: image.vmAddress,
205-
address: image.address,
206-
size: image.size,
207-
uuid: image.uuid)
203+
SentryDependencyContainer.sharedInstance().binaryImageCache.binaryImageAdded(image: image)
208204

209205
let sut = fixture.sut
210206
sut.start()

0 commit comments

Comments
 (0)