From db2fabb965eb6a1f4e4a083bbbeb7cc2e3b28c33 Mon Sep 17 00:00:00 2001 From: Mikhail Yarmaliuk Date: Thu, 30 Apr 2026 19:51:49 -0700 Subject: [PATCH] fix(ios): prevent crash in saveFailedUpdate when server returns null fields Filter NSNull from the failed package dictionary before storing it in NSUserDefaults, and wrap the write in @try/@catch as a defensive guard. The server may return null for optional metadata fields (e.g. assetDownloadUrl, bundleDiffBlobUrl). NSJSONSerialization parses these to NSNull, which is not a property-list type. Passing such a dictionary to -[NSUserDefaults setObject:forKey:] raises NSInvalidArgumentException ("Attempt to insert non-property list object"). Because saveFailedUpdate: is invoked from -[CodePush init] via initializeUpdateAfterRestart -> rollbackPackage, an uncaught throw here aborts the process during native module instantiation - i.e. the app crashes on every launch until the corrupt state is cleared by uninstall + reinstall. The dictionary stored in failedUpdates is only consumed by +isFailedHash:, which reads the packageHash field (always an NSString), so dropping the URL/diff fields has no functional impact. --- ios/CodePush/CodePush.m | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/ios/CodePush/CodePush.m b/ios/CodePush/CodePush.m index b58d21864..e6d91a660 100644 --- a/ios/CodePush/CodePush.m +++ b/ios/CodePush/CodePush.m @@ -583,7 +583,19 @@ - (void)saveFailedUpdate:(NSDictionary *)failedPackage if ([[self class] isFailedHash:[failedPackage objectForKey:PackageHashKey]]) { return; } - + + // The server may return null for optional fields (e.g. assetDownloadUrl, + // bundleDiffBlobUrl), which NSJSONSerialization parses to NSNull. NSNull + // is not a property list type, so passing it to NSUserDefaults raises + // NSInvalidArgumentException ("Attempt to insert non-property list object"). + NSMutableDictionary *sanitizedPackage = [NSMutableDictionary dictionaryWithCapacity:failedPackage.count]; + for (NSString *key in failedPackage) { + id value = failedPackage[key]; + if (value && ![value isKindOfClass:[NSNull class]]) { + sanitizedPackage[key] = value; + } + } + NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; NSMutableArray *failedUpdates = [preferences objectForKey:FailedUpdatesKey]; if (failedUpdates == nil) { @@ -594,9 +606,18 @@ - (void)saveFailedUpdate:(NSDictionary *)failedPackage failedUpdates = [failedUpdates mutableCopy]; } - [failedUpdates addObject:failedPackage]; - [preferences setObject:failedUpdates forKey:FailedUpdatesKey]; - [preferences synchronize]; + [failedUpdates addObject:sanitizedPackage]; + @try { + [preferences setObject:failedUpdates forKey:FailedUpdatesKey]; + [preferences synchronize]; + } @catch (NSException *exception) { + // Defensive: if a future server response shape slips through the filter, + // clear the corrupt list so the app does not crash on every subsequent + // launch via initializeUpdateAfterRestart -> rollbackPackage. + CPLog(@"Failed to persist failed update list: %@. Clearing key.", exception); + [preferences removeObjectForKey:FailedUpdatesKey]; + [preferences synchronize]; + } } /*