From 005eb841567befda08619781e992368bf36cb03b Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 23 Sep 2026 18:19:25 -0700 Subject: [PATCH 1/2] fix: [SDK-5314] drop the Identity Verification beta queues and guard cached queue blobs --- .../Source/OneSignalCommonDefines.h | 12 ++++ .../Source/OneSignalUserDefaults.h | 2 + .../Source/OneSignalUserDefaults.m | 55 +++++++++++++++++-- .../OneSignalCoreTests.swift | 53 ++++++++++++++++++ .../Source/OSOperationRepo.swift | 2 +- .../Executors/OSCustomEventsExecutor.swift | 4 +- .../OSIdentityOperationExecutor.swift | 6 +- .../OSPropertyOperationExecutor.swift | 4 +- .../OSSubscriptionOperationExecutor.swift | 8 +-- .../Source/Executors/OSUserExecutor.swift | 2 +- .../Source/OneSignalUserManagerImpl.swift | 19 +++++++ .../OneSignalUserTests.swift | 33 +++++++++++ 12 files changed, 181 insertions(+), 19 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index 0c3b6cd960..a31abb5c61 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -378,6 +378,18 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP #define OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY" #define OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY" +// A cached queue above this size is dropped instead of decoded; real queues are a few KB. +#define OS_CACHED_QUEUE_MAX_BYTES 1048576 +// CFPreferences rejects a single value at or above this size on iOS. +#define OS_USER_DEFAULTS_MAX_VALUE_BYTES 4194304 + +// Written only by the 5.3.0-beta Identity Verification builds; nothing reads them, so start() removes them. +#define OS_IV_BETA_USER_EXECUTOR_PENDING_QUEUE_KEY @"OS_USER_EXECUTOR_PENDING_QUEUE_KEY" +#define OS_IV_BETA_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY @"OS_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY" +#define OS_IV_BETA_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY @"OS_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY" +#define OS_IV_BETA_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY" +#define OS_IV_BETA_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY" + // Live Activies Executor #define OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY" #define OS_LIVE_ACTIVITIES_EXECUTOR_START_TOKENS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_START_TOKENS_KEY" diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.h index 89a03fe3e1..4a7269a2b9 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.h @@ -70,6 +70,8 @@ // NSUserDefaults for storing and getting saved codeable data (custom objects) - (id _Nullable)getSavedCodeableDataForKey:(NSString * _Nonnull)key defaultValue:(id _Nullable)value; +/// Same, but a stored blob larger than `maxBytes` is removed and `value` returned instead of being decoded. +- (id _Nullable)getSavedCodeableDataForKey:(NSString * _Nonnull)key defaultValue:(id _Nullable)value maxBytes:(NSUInteger)maxBytes; - (void)saveCodeableDataForKey:(NSString * _Nonnull)key withValue:(id _Nullable)value; @end diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.m index fb48bacf55..605f971182 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.m +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.m @@ -28,6 +28,7 @@ #import #import "OneSignalUserDefaults.h" #import "OneSignalCommonDefines.h" +#import "OneSignalLog.h" @implementation OneSignalUserDefaults : NSObject @@ -152,15 +153,57 @@ - (void)saveObjectForKey:(NSString * _Nonnull)key withValue:(id _Nullable)object } - (id _Nullable)getSavedCodeableDataForKey:(NSString * _Nonnull)key defaultValue:(id _Nullable)value { - if ([self keyExists:key]) - return [NSKeyedUnarchiver unarchiveObjectWithData:[self.userDefaults objectForKey:key]]; - - return value; + return [self getSavedCodeableDataForKey:key defaultValue:value maxBytes:0]; +} + +- (id _Nullable)getSavedCodeableDataForKey:(NSString * _Nonnull)key defaultValue:(id _Nullable)value maxBytes:(NSUInteger)maxBytes { + if (![self keyExists:key]) + return value; + + id stored = [self.userDefaults objectForKey:key]; + if (![stored isKindOfClass:[NSData class]]) { + [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults dropping %@: not archived data", key]]; + [self removeValueForKey:key]; + return value; + } + NSData *data = stored; + if (maxBytes > 0 && data.length > maxBytes) { + [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults dropping %@: %lu bytes is over the %lu byte limit", key, (unsigned long)data.length, (unsigned long)maxBytes]]; + [self removeValueForKey:key]; + return value; + } + @try { + // An archive can decode to nothing; hand back the default the same way a missing key does. + return [NSKeyedUnarchiver unarchiveObjectWithData:data] ?: value; + } @catch (NSException *exception) { + [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults dropping %@: %@", key, exception.reason]]; + [self removeValueForKey:key]; + return value; + } } - (void)saveCodeableDataForKey:(NSString * _Nonnull)key withValue:(id _Nullable)value { - [self.userDefaults setObject:[NSKeyedArchiver archivedDataWithRootObject:value] forKey:key]; - [self.userDefaults synchronize]; + NSData *data; + @try { + data = [NSKeyedArchiver archivedDataWithRootObject:value]; + } @catch (NSException *exception) { + // The old blob no longer matches memory, so drop it rather than restore it next launch. + [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults could not archive %@, removing it: %@", key, exception.reason]]; + [self removeValueForKey:key]; + return; + } + // CFPreferences refuses a value this large and may stop persisting the suite afterwards. + if (data.length >= OS_USER_DEFAULTS_MAX_VALUE_BYTES) { + [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults not caching %@: %lu bytes", key, (unsigned long)data.length]]; + [self removeValueForKey:key]; + return; + } + @try { + [self.userDefaults setObject:data forKey:key]; + [self.userDefaults synchronize]; + } @catch (NSException *exception) { + [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults could not cache %@: %@", key, exception.reason]]; + } } //gets the NSBundle of the primary application - NOT the app extension diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreTests/OneSignalCoreTests.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreTests/OneSignalCoreTests.swift index 7187b5f0cd..595bdb0257 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreTests/OneSignalCoreTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreTests/OneSignalCoreTests.swift @@ -73,4 +73,57 @@ final class OneSignalCoreTests: XCTestCase { XCTAssertEqual(templateId, "templateId123") XCTAssertEqual(templateName, "Template name") } + + // MARK: - Codeable data guards + + private let guardKey = "OS_CORE_TESTS_CODEABLE_GUARD" + + func testOversizedCodeableDataIsDroppedNotDecoded() { + let defaults = OneSignalUserDefaults.initShared() + defaults.saveObject(forKey: guardKey, withValue: Data(count: 1025)) + + let restored = defaults.getSavedCodeableData(forKey: guardKey, defaultValue: ["default"], maxBytes: 1024) as? [String] + + XCTAssertEqual(restored, ["default"]) + XCTAssertFalse(defaults.keyExists(guardKey)) + } + + func testCodeableDataWithinTheLimitIsDecoded() { + let defaults = OneSignalUserDefaults.initShared() + defaults.saveCodeableData(forKey: guardKey, withValue: ["cached"]) + + let restored = defaults.getSavedCodeableData(forKey: guardKey, defaultValue: [], maxBytes: 1024) as? [String] + + XCTAssertEqual(restored, ["cached"]) + defaults.removeValue(forKey: guardKey) + } + + func testUnreadableCodeableDataReturnsTheDefaultWithoutThrowing() { + let defaults = OneSignalUserDefaults.initShared() + defaults.saveObject(forKey: guardKey, withValue: Data("not an archive".utf8)) + + let restored = defaults.getSavedCodeableData(forKey: guardKey, defaultValue: ["default"]) as? [String] + + XCTAssertEqual(restored, ["default"]) + defaults.removeValue(forKey: guardKey) + } + + func testUnarchivableValueIsNotCachedAndClearsTheKey() { + let defaults = OneSignalUserDefaults.initShared() + defaults.saveCodeableData(forKey: guardKey, withValue: ["cached"]) + + // NSObject does not adopt NSCoding, so archiving it raises. + defaults.saveCodeableData(forKey: guardKey, withValue: NSObject()) + + XCTAssertFalse(defaults.keyExists(guardKey)) + } + + func testValueAtThePlatformLimitIsNotCached() { + let defaults = OneSignalUserDefaults.initShared() + defaults.saveCodeableData(forKey: guardKey, withValue: ["cached"]) + + defaults.saveCodeableData(forKey: guardKey, withValue: Data(count: Int(OS_USER_DEFAULTS_MAX_VALUE_BYTES))) + + XCTAssertFalse(defaults.keyExists(guardKey)) + } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift index d1b3a36e8d..851a99c8c1 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift @@ -78,7 +78,7 @@ public class OSOperationRepo: NSObject { } private func uncacheDeltaQueue() { - guard let cached = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_OPERATION_REPO_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] else { + guard let cached = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_OPERATION_REPO_DELTA_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSDelta] else { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSOperationRepo is unable to uncache the OSDelta queue.") return } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSCustomEventsExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSCustomEventsExecutor.swift index b6571ac08a..b65c2b9091 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSCustomEventsExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSCustomEventsExecutor.swift @@ -63,7 +63,7 @@ class OSCustomEventsExecutor: OSOperationExecutor { } private func uncacheDeltas() { - if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] { + if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSDelta] { for (index, delta) in deltaQueue.enumerated().reversed() { if OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) == nil { // The identity model does not exist, drop this Delta @@ -81,7 +81,7 @@ class OSCustomEventsExecutor: OSOperationExecutor { } private func uncacheRequests() { - if var requestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestCustomEvents] { + if var requestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSRequestCustomEvents] { // Hook each uncached Request to the model in the store for (index, request) in requestQueue.enumerated().reversed() { if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift index c3e1825bc0..3435a77a9e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift @@ -50,7 +50,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } private func uncacheDeltas() { - if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] { + if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSDelta] { // Hook each uncached Delta to the model in the store for (index, delta) in deltaQueue.enumerated().reversed() { if let modelInStore = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.model.modelId) { @@ -70,7 +70,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } private func uncacheAddAliasRequests() { - if var addRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestAddAliases] { + if var addRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSRequestAddAliases] { // Hook each uncached Request to the model in the store for (index, request) in addRequestQueue.enumerated().reversed() { if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { @@ -93,7 +93,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } private func uncacheRemoveAliasRequests() { - if var removeRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestRemoveAlias] { + if var removeRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSRequestRemoveAlias] { // Hook each uncached Request to the model in the store for (index, request) in removeRequestQueue.enumerated().reversed() { if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index 4dbb9e25c8..d50b13ee51 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -85,7 +85,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } private func uncacheDeltas() { - if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] { + if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSDelta] { for (index, delta) in deltaQueue.enumerated().reversed() { if OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) == nil { // The identity model does not exist, drop this Delta @@ -101,7 +101,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } private func uncacheUpdateRequests() { - if var updateRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestUpdateProperties] { + if var updateRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSRequestUpdateProperties] { // Hook each uncached Request to the model in the store for (index, request) in updateRequestQueue.enumerated().reversed() { if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift index aeee2c0992..c7d7ae68df 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift @@ -53,7 +53,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } private func uncacheDeltas() { - if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] { + if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSDelta] { // Hook each uncached Delta to the model in the store for (index, delta) in deltaQueue.enumerated().reversed() { if let modelInStore = getSubscriptionModelFromStores(modelId: delta.model.modelId) { @@ -75,7 +75,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { private func uncacheCreateSubscriptionRequests() { var requestQueue: [OSRequestCreateSubscription] = [] - if let cachedAddRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestCreateSubscription] { + if let cachedAddRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSRequestCreateSubscription] { // Hook each uncached Request to the model in the store for request in cachedAddRequestQueue { // 1. Hook up the subscription model @@ -111,7 +111,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } private func uncacheDeleteSubscriptionRequests() { - if var removeRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestDeleteSubscription] { + if var removeRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSRequestDeleteSubscription] { // Hook each uncached Request to the model in the store for (index, request) in removeRequestQueue.enumerated().reversed() { if let subscriptionModel = getSubscriptionModelFromStores(modelId: request.subscriptionModel.modelId) { @@ -136,7 +136,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } private func uncacheUpdateSubscriptionRequests() { - if var updateRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestUpdateSubscription] { + if var updateRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSRequestUpdateSubscription] { // Hook each uncached Request to the model in the store for (index, request) in updateRequestQueue.enumerated().reversed() { if let subscriptionModel = getSubscriptionModelFromStores(modelId: request.subscriptionModel.modelId) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 392fb34081..6e06cb820a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -167,7 +167,7 @@ class OSUserExecutor { var userRequestQueue: [OSUserRequest] = [] // Read unfinished Create User + Identify User + Get Identity By Subscription requests from cache, if any... - if let cachedRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSUserRequest] { + if let cachedRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, defaultValue: [], maxBytes: UInt(OS_CACHED_QUEUE_MAX_BYTES)) as? [OSUserRequest] { // Hook each uncached Request to the right model reference for request in cachedRequestQueue { if request.isKind(of: OSRequestFetchIdentityBySubscription.self), let req = request as? OSRequestFetchIdentityBySubscription { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 652c7836ef..a1a4573b6a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -238,6 +238,23 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { self.pushSubscriptionImpl = OSPushSubscriptionImpl(pushSubscriptionModelStore: pushSubscriptionModelStore) } + /// The 5.3.0-beta builds parked Requests awaiting a JWT under these keys and never trimmed them, so an + /// upgrading device can carry a large blob nothing reads anymore. + private func removeIdentityVerificationBetaCaches() { + let defaults = OneSignalUserDefaults.initShared() + let keys = [ + OS_IV_BETA_USER_EXECUTOR_PENDING_QUEUE_KEY, + OS_IV_BETA_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY, + OS_IV_BETA_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY, + OS_IV_BETA_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY, + OS_IV_BETA_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY + ] + for key in keys where defaults.keyExists(key) { + OneSignalLog.onesignalLog(.LL_DEBUG, message: "OneSignalUserManager removing the Identity Verification beta cache \(key)") + defaults.removeValue(forKey: key) + } + } + @objc public func start() { guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: nil) else { @@ -307,6 +324,8 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { self?.pushSubscriptionModelStore.getModel(key: OS_PUSH_SUBSCRIPTION_MODEL_KEY)?._isDisabledInternally = false } + removeIdentityVerificationBetaCaches() + // Setup the executors // The OSUserExecutor has to run first, before other executors self.userExecutor = OSUserExecutor(newRecordsState: newRecordsState, identityVerificationService: identityVerificationService, auth: requestAuth) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift index 2a4f8ebb80..fc361a4194 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift @@ -850,4 +850,37 @@ private class SpyModelChangedHandler: OSModelChangedHandler { serverUpdates.append(args.property) } } + + func testStartRemovesIdentityVerificationBetaCachesAndAnOversizedQueue() throws { + let client = MockOneSignalClient() + client.executeInstantaneously = true + MockUserRequests.setDefaultCreateAnonUserResponses(with: client) + OneSignalCoreImpl.setSharedClient(client) + + let defaults = OneSignalUserDefaults.initShared() + let betaKeys = [ + OS_IV_BETA_USER_EXECUTOR_PENDING_QUEUE_KEY, + OS_IV_BETA_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY, + OS_IV_BETA_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY, + OS_IV_BETA_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY, + OS_IV_BETA_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY + ] + for key in betaKeys { + defaults.saveCodeableData(forKey: key, withValue: ["nan01": []]) + } + // A queue blob a defect left behind, too large to be worth decoding at launch. + defaults.saveObject(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: Data(count: Int(OS_CACHED_QUEUE_MAX_BYTES) + 1)) + + OneSignalUserManagerImpl.sharedInstance.start() + OneSignalCoreMocks.waitUntil("Anonymous user creation did not complete") { + client.hasCompletedRequestOfType(OSRequestCreateUser.self) + } + + for key in betaKeys { + XCTAssertFalse(defaults.keyExists(key), "\(key) survived start()") + } + // The property executor replaced the blob with its empty queue rather than decoding it. + let queue = defaults.getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, defaultValue: nil) as? [Any] + XCTAssertEqual(queue?.count, 0) + } } From d3b0d913b70e9925f23788dc3b4d350cad249b04 Mon Sep 17 00:00:00 2001 From: Nan Date: Thu, 24 Sep 2026 08:42:19 -0700 Subject: [PATCH 2/2] chore: [SDK-5314] reword the cache guard comments --- .../OneSignalCore/Source/OneSignalCommonDefines.h | 4 ++-- .../OneSignalCore/Source/OneSignalUserDefaults.h | 2 +- .../OneSignalCore/Source/OneSignalUserDefaults.m | 6 +++--- .../OneSignalUserTests/OneSignalUserTests.swift | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index a31abb5c61..a621afe313 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -378,12 +378,12 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP #define OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY" #define OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY" -// A cached queue above this size is dropped instead of decoded; real queues are a few KB. +// A cached queue past this size is dropped without decoding it. Real queues are a few KB. #define OS_CACHED_QUEUE_MAX_BYTES 1048576 // CFPreferences rejects a single value at or above this size on iOS. #define OS_USER_DEFAULTS_MAX_VALUE_BYTES 4194304 -// Written only by the 5.3.0-beta Identity Verification builds; nothing reads them, so start() removes them. +// Written only by the 5.3.0-beta Identity Verification builds. Nothing reads them anymore. #define OS_IV_BETA_USER_EXECUTOR_PENDING_QUEUE_KEY @"OS_USER_EXECUTOR_PENDING_QUEUE_KEY" #define OS_IV_BETA_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY @"OS_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY" #define OS_IV_BETA_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY @"OS_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY" diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.h index 4a7269a2b9..8f0b54ade8 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.h @@ -70,7 +70,7 @@ // NSUserDefaults for storing and getting saved codeable data (custom objects) - (id _Nullable)getSavedCodeableDataForKey:(NSString * _Nonnull)key defaultValue:(id _Nullable)value; -/// Same, but a stored blob larger than `maxBytes` is removed and `value` returned instead of being decoded. +/// Removes a stored blob larger than `maxBytes` and returns `value` instead of decoding it. - (id _Nullable)getSavedCodeableDataForKey:(NSString * _Nonnull)key defaultValue:(id _Nullable)value maxBytes:(NSUInteger)maxBytes; - (void)saveCodeableDataForKey:(NSString * _Nonnull)key withValue:(id _Nullable)value; diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.m index 605f971182..3d4af4b87f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.m +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.m @@ -168,12 +168,12 @@ - (id _Nullable)getSavedCodeableDataForKey:(NSString * _Nonnull)key defaultValue } NSData *data = stored; if (maxBytes > 0 && data.length > maxBytes) { - [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults dropping %@: %lu bytes is over the %lu byte limit", key, (unsigned long)data.length, (unsigned long)maxBytes]]; + [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults dropping %@: %lu bytes, limit %lu", key, (unsigned long)data.length, (unsigned long)maxBytes]]; [self removeValueForKey:key]; return value; } @try { - // An archive can decode to nothing; hand back the default the same way a missing key does. + // A blob that decodes to nil is treated like a missing key. return [NSKeyedUnarchiver unarchiveObjectWithData:data] ?: value; } @catch (NSException *exception) { [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults dropping %@: %@", key, exception.reason]]; @@ -187,7 +187,7 @@ - (void)saveCodeableDataForKey:(NSString * _Nonnull)key withValue:(id _Nullable) @try { data = [NSKeyedArchiver archivedDataWithRootObject:value]; } @catch (NSException *exception) { - // The old blob no longer matches memory, so drop it rather than restore it next launch. + // The old blob no longer matches memory, so drop it rather than restore it on the next launch. [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"OneSignalUserDefaults could not archive %@, removing it: %@", key, exception.reason]]; [self removeValueForKey:key]; return; diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift index fc361a4194..afa6b2f89a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift @@ -868,7 +868,7 @@ private class SpyModelChangedHandler: OSModelChangedHandler { for key in betaKeys { defaults.saveCodeableData(forKey: key, withValue: ["nan01": []]) } - // A queue blob a defect left behind, too large to be worth decoding at launch. + // A queue blob too large to decode at launch. defaults.saveObject(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: Data(count: Int(OS_CACHED_QUEUE_MAX_BYTES) + 1)) OneSignalUserManagerImpl.sharedInstance.start()