Skip to content

Commit f1915ee

Browse files
committed
fix: app crash from [MPUpload description]
Stacktrace ``` 0 libsystem_malloc.dylib 0x30a0 _xzm_xzone_malloc_freelist_outlined + 864 1 Foundation 0x4804 -[NSString quotedStringRepresentation] + 132 2 Foundation 0x4724 -[NSString _stringRepresentation] + 360 3 CoreFoundation 0x14a580 -[NSDictionary descriptionWithLocale:indent:] + 1128 4 CoreFoundation 0x14a5ac -[NSDictionary descriptionWithLocale:indent:] + 1172 5 Foundation 0x36b4 _NSDescriptionWithLocaleFunc + 56 6 CoreFoundation 0x15cb0 __CFSTRING_IS_CALLING_OUT_TO_AN_OBJECT_FORMAT_ARGUMENT_WITH_LOCALE__ + 28 7 CoreFoundation 0x8c4c __CFStringAppendFormatCore + 9968 8 CoreFoundation 0x122e4 _CFStringCreateWithFormatAndArgumentsReturningMetadata + 184 9 CoreFoundation 0x12398 _CFStringCreateWithFormatAndArgumentsAux2 + 44 10 Foundation 0x9b2bb0 +[NSString stringWithFormat:] + 68 11 mParticle_Apple_SDK 0x3b7c8 -[MPUpload description] + 172 12 mParticle_Apple_SDK 0x232c4 -[MPPersistenceController_PRIVATE saveUpload:] + 1472 13 mParticle_Apple_SDK 0x94d1c -[MPUploadBuilder build:] + 3488 14 mParticle_Apple_SDK 0x6e30c __55-[MPBackendController_PRIVATE prepareBatchesForUpload:]_block_invoke_4 + 548 ``` * Use serializedString (raw JSON) instead of dictionaryRepresentation to avoid deserializing and then re-describing the dictionary, which can trigger crashes * Add try catch creating upload description
1 parent 6aec803 commit f1915ee

4 files changed

Lines changed: 154 additions & 3 deletions

File tree

UnitTests/ObjCTests/MPDataModelTests.m

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,81 @@ - (void)testUploadInstance {
227227
XCTAssertNotEqualObjects(uploadCopy, upload, @"Should not have been equal.");
228228
}
229229

230+
- (void)testUploadDescriptionUsesSerializedString {
231+
MPSession *session = [[MPSession alloc] initWithStartTime:[[NSDate date] timeIntervalSince1970] userId:[MPPersistenceController_PRIVATE mpId]];
232+
233+
MPMessageBuilder *messageBuilder = [[MPMessageBuilder alloc] initWithMessageType:MPMessageTypeEvent
234+
session:session
235+
messageInfo:@{@"MessageKey1":@"MessageValue1"}];
236+
MPMessage *message = [messageBuilder build];
237+
238+
NSDictionary *uploadDictionary = @{kMPOptOutKey:@NO,
239+
kMPSessionTimeoutKey:@120,
240+
kMPUploadIntervalKey:@10,
241+
kMPLifeTimeValueKey:@0,
242+
kMPMessagesKey:@[[message dictionaryRepresentation]],
243+
kMPMessageIdKey:[[NSUUID UUID] UUIDString]};
244+
245+
MPUpload *upload = [[MPUpload alloc] initWithSessionId:[NSNumber numberWithLongLong:session.sessionId] uploadDictionary:uploadDictionary dataPlanId:@"test" dataPlanVersion:@(1) uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
246+
247+
NSString *description = [upload description];
248+
XCTAssertNotNil(description, @"Description should not have been nil.");
249+
250+
NSString *serialized = [upload serializedString];
251+
XCTAssertNotNil(serialized, @"Serialized string should not have been nil.");
252+
XCTAssertTrue([description containsString:serialized], @"Description should contain the raw JSON serialized string.");
253+
}
254+
255+
- (void)testUploadDescriptionWithNilDataPlan {
256+
MPSession *session = [[MPSession alloc] initWithStartTime:[[NSDate date] timeIntervalSince1970] userId:[MPPersistenceController_PRIVATE mpId]];
257+
258+
MPMessageBuilder *messageBuilder = [[MPMessageBuilder alloc] initWithMessageType:MPMessageTypeEvent
259+
session:session
260+
messageInfo:@{@"MessageKey1":@"MessageValue1"}];
261+
MPMessage *message = [messageBuilder build];
262+
263+
NSDictionary *uploadDictionary = @{kMPOptOutKey:@NO,
264+
kMPSessionTimeoutKey:@120,
265+
kMPUploadIntervalKey:@10,
266+
kMPLifeTimeValueKey:@0,
267+
kMPMessagesKey:@[[message dictionaryRepresentation]],
268+
kMPMessageIdKey:[[NSUUID UUID] UUIDString]};
269+
270+
MPUpload *upload = [[MPUpload alloc] initWithSessionId:[NSNumber numberWithLongLong:session.sessionId] uploadDictionary:uploadDictionary dataPlanId:nil dataPlanVersion:nil uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
271+
272+
NSString *description = [upload description];
273+
XCTAssertNotNil(description, @"Description should not have been nil even with nil data plan fields.");
274+
XCTAssertTrue([description containsString:@"<nil>"], @"Description should contain '<nil>' placeholder for nil data plan fields.");
275+
}
276+
277+
- (void)testUploadDescriptionWithNestedDictionary {
278+
MPSession *session = [[MPSession alloc] initWithStartTime:[[NSDate date] timeIntervalSince1970] userId:[MPPersistenceController_PRIVATE mpId]];
279+
280+
MPMessageBuilder *messageBuilder = [[MPMessageBuilder alloc] initWithMessageType:MPMessageTypeEvent
281+
session:session
282+
messageInfo:@{@"MessageKey1":@"MessageValue1"}];
283+
MPMessage *message = [messageBuilder build];
284+
285+
// Build an upload dictionary with deeply nested content to simulate
286+
// the kind of data that could come from kits like Rokt
287+
NSDictionary *uploadDictionary = @{kMPOptOutKey:@NO,
288+
kMPSessionTimeoutKey:@120,
289+
kMPUploadIntervalKey:@10,
290+
kMPLifeTimeValueKey:@0,
291+
kMPMessagesKey:@[[message dictionaryRepresentation]],
292+
kMPMessageIdKey:[[NSUUID UUID] UUIDString],
293+
kMPUserAttributeKey:@{
294+
@"email":@"test@example.com",
295+
@"nested":@{@"key1":@"val1", @"key2":@"val2"},
296+
@"list":@[@"a", @"b", @"c"]
297+
}};
298+
299+
MPUpload *upload = [[MPUpload alloc] initWithSessionId:[NSNumber numberWithLongLong:session.sessionId] uploadDictionary:uploadDictionary dataPlanId:@"test" dataPlanVersion:@(1) uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
300+
301+
NSString *description = [upload description];
302+
XCTAssertNotNil(description, @"Description should not crash with nested dictionary data.");
303+
}
304+
230305
- (void)testBreadcrumbInstance {
231306
MPSession *session = [[MPSession alloc] initWithStartTime:[[NSDate date] timeIntervalSince1970] userId:[MPPersistenceController_PRIVATE mpId]];
232307

UnitTests/ObjCTests/MPPersistenceControllerTests.mm

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,70 @@ - (void)testUploadWithOptOutMessage {
551551
[self waitForExpectationsWithTimeout:DEFAULT_TIMEOUT handler:nil];
552552
}
553553

554+
- (void)testSaveUploadDoesNotCrashOnDescription {
555+
MPSession *session = [[MPSession alloc] initWithStartTime:[[NSDate date] timeIntervalSince1970] userId:[MPPersistenceController_PRIVATE mpId]];
556+
557+
MPMessageBuilder *messageBuilder = [[MPMessageBuilder alloc] initWithMessageType:MPMessageTypeEvent
558+
session:session
559+
messageInfo:@{@"MessageKey1":@"MessageValue1"}];
560+
MPMessage *message = [messageBuilder build];
561+
562+
NSDictionary *uploadDictionary = @{kMPOptOutKey:@NO,
563+
kMPSessionTimeoutKey:@120,
564+
kMPUploadIntervalKey:@10,
565+
kMPLifeTimeValueKey:@0,
566+
kMPMessagesKey:@[[message dictionaryRepresentation]],
567+
kMPMessageIdKey:[[NSUUID UUID] UUIDString],
568+
kMPUserAttributeKey:@{
569+
@"email":@"test@example.com",
570+
@"nested":@{@"key1":@"val1"},
571+
@"list":@[@"item1", @"item2"]
572+
}};
573+
574+
MPUpload *upload = [[MPUpload alloc] initWithSessionId:[NSNumber numberWithLongLong:session.sessionId] uploadDictionary:uploadDictionary dataPlanId:@"test" dataPlanVersion:@(1) uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
575+
576+
MPPersistenceController_PRIVATE *persistence = [MParticle sharedInstance].persistenceController;
577+
578+
[persistence saveUpload:upload];
579+
580+
XCTAssertTrue(upload.uploadId > 0, @"Upload id not greater than zero: %lld", upload.uploadId);
581+
582+
NSArray<MPUpload *> *uploads = [persistence fetchUploads];
583+
MPUpload *fetchedUpload = [uploads lastObject];
584+
XCTAssertEqualObjects(upload, fetchedUpload, @"Upload and fetchedUpload are not equal.");
585+
586+
NSString *description = [fetchedUpload description];
587+
XCTAssertNotNil(description, @"Fetched upload description should not be nil.");
588+
589+
[persistence deleteUpload:upload];
590+
}
591+
592+
- (void)testSaveUploadWithNilFieldsDoesNotCrash {
593+
MPSession *session = [[MPSession alloc] initWithStartTime:[[NSDate date] timeIntervalSince1970] userId:[MPPersistenceController_PRIVATE mpId]];
594+
595+
MPMessageBuilder *messageBuilder = [[MPMessageBuilder alloc] initWithMessageType:MPMessageTypeEvent
596+
session:session
597+
messageInfo:@{@"MessageKey1":@"MessageValue1"}];
598+
MPMessage *message = [messageBuilder build];
599+
600+
NSDictionary *uploadDictionary = @{kMPOptOutKey:@NO,
601+
kMPSessionTimeoutKey:@120,
602+
kMPUploadIntervalKey:@10,
603+
kMPLifeTimeValueKey:@0,
604+
kMPMessagesKey:@[[message dictionaryRepresentation]],
605+
kMPMessageIdKey:[[NSUUID UUID] UUIDString]};
606+
607+
MPUpload *upload = [[MPUpload alloc] initWithSessionId:[NSNumber numberWithLongLong:session.sessionId] uploadDictionary:uploadDictionary dataPlanId:nil dataPlanVersion:nil uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
608+
609+
MPPersistenceController_PRIVATE *persistence = [MParticle sharedInstance].persistenceController;
610+
611+
[persistence saveUpload:upload];
612+
613+
XCTAssertTrue(upload.uploadId > 0, @"Upload id not greater than zero: %lld", upload.uploadId);
614+
615+
[persistence deleteUpload:upload];
616+
}
617+
554618
- (void)testAudiences {
555619
MPAudience *audience = [[MPAudience alloc] initWithAudienceId:@2];
556620
XCTAssertTrue(audience.audienceId.intValue == 2);

mParticle-Apple-SDK/Data Model/MPUpload.m

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,15 @@ - (instancetype)initWithSessionId:(NSNumber *)sessionId uploadId:(int64_t)upload
4242
}
4343

4444
- (NSString *)description {
45-
NSDictionary *dictionaryRepresentation = [self dictionaryRepresentation];
45+
NSString *content = [self serializedString] ?: @"<nil>";
4646

47-
return [NSString stringWithFormat:@"Upload\n Id: %lld\n UUID: %@\n Content: %@\n timestamp: %.0f\n Data Plan: %@ %@\n", self.uploadId, self.uuid, dictionaryRepresentation, self.timestamp, self.dataPlanId, self.dataPlanVersion];
47+
return [NSString stringWithFormat:@"Upload\n Id: %lld\n UUID: %@\n Content: %@\n timestamp: %.0f\n Data Plan: %@ %@\n",
48+
self.uploadId,
49+
self.uuid ?: @"<nil>",
50+
content,
51+
self.timestamp,
52+
self.dataPlanId ?: @"<nil>",
53+
self.dataPlanVersion ?: @"<nil>"];
4854
}
4955

5056
- (BOOL)isEqual:(MPUpload *)object {

mParticle-Apple-SDK/Persistence/MPPersistenceController.mm

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1727,7 +1727,13 @@ - (void)saveUpload:(nonnull MPUpload *)upload {
17271727

17281728
upload.uploadId = sqlite3_last_insert_rowid(mParticleDB);
17291729

1730-
[MPListenerController.sharedInstance onEntityStored:MPDatabaseTableUploads primaryKey:@(upload.uploadId) message:upload.description];
1730+
@try {
1731+
NSString *desc = upload.description ?: @"<unavailable>";
1732+
[MPListenerController.sharedInstance onEntityStored:MPDatabaseTableUploads primaryKey:@(upload.uploadId) message:desc];
1733+
} @catch (NSException *exception) {
1734+
MPILogError(@"Error generating upload description: %@", exception);
1735+
[MPListenerController.sharedInstance onEntityStored:MPDatabaseTableUploads primaryKey:@(upload.uploadId) message:@"<description unavailable>"];
1736+
}
17311737

17321738
sqlite3_clear_bindings(preparedStatement);
17331739
} else {

0 commit comments

Comments
 (0)