From 77b42e097e23ca488a82ab78f9b9b55d3beaaa6d Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 19 Dec 2024 10:52:54 +0100 Subject: [PATCH 1/9] feat: add method unswizzling --- CHANGELOG.md | 1 + Sources/Sentry/SentrySwizzle.m | 92 ++++++++++++++++++- .../include/HybridPublic/SentrySwizzle.h | 29 ++++++ Tests/SentryTests/Helper/SentrySwizzleTests.m | 23 +++++ scripts/.clang-format-version | 2 +- 5 files changed, 144 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94661dc0431..4b95549b784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - `SentrySdkInfo.packages` should be an array (#4626) - Use the same SdkInfo for envelope header and event (#4629) +- Add method unswizzling ### Internal diff --git a/Sources/Sentry/SentrySwizzle.m b/Sources/Sentry/SentrySwizzle.m index 13c37553557..6a37ee95ee9 100644 --- a/Sources/Sentry/SentrySwizzle.m +++ b/Sources/Sentry/SentrySwizzle.m @@ -40,8 +40,46 @@ - (SentrySwizzleOriginalIMP)getOriginalImplementation @implementation SentrySwizzle +static NSMutableDictionary * +refsToOriginalImplementationsDictionary(void) +{ + static NSMutableDictionary *refsToOriginalImplementations; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ refsToOriginalImplementations = [NSMutableDictionary new]; }); + return refsToOriginalImplementations; +} + +static void +storeRefToOriginalImplementation(const void *key, IMP implementation) +{ + NSMutableDictionary *refsToOriginalImplementations + = refsToOriginalImplementationsDictionary(); + NSValue *keyValue = [NSValue valueWithPointer:key]; + refsToOriginalImplementations[keyValue] = [NSValue valueWithPointer:implementation]; +} + +static void +removeRefToOriginalImplementation(const void *key) +{ + NSMutableDictionary *refsToOriginalImplementations + = refsToOriginalImplementationsDictionary(); + NSValue *keyValue = [NSValue valueWithPointer:key]; + [refsToOriginalImplementations removeObjectForKey:keyValue]; +} + +static IMP +getRefToOriginalImplementation(const void *key) +{ + NSMutableDictionary *refsToOriginalImplementations + = refsToOriginalImplementationsDictionary(); + NSValue *keyValue = [NSValue valueWithPointer:key]; + NSValue *originalImplementationValue = [refsToOriginalImplementations objectForKey:keyValue]; + return (IMP)[originalImplementationValue pointerValue]; +} + static void -swizzle(Class classToSwizzle, SEL selector, SentrySwizzleImpFactoryBlock factoryBlock) +swizzle( + Class classToSwizzle, SEL selector, SentrySwizzleImpFactoryBlock factoryBlock, const void *key) { Method method = class_getInstanceMethod(classToSwizzle, selector); @@ -106,6 +144,33 @@ @implementation SentrySwizzle pthread_mutex_lock(&gLock); originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType); + if (originalIMP) { + storeRefToOriginalImplementation(key, originalIMP); + } + + pthread_mutex_unlock(&gLock); +} + +static void +unswizzle(Class classToUnswizzle, SEL selector, const void *key) +{ + Method method = class_getInstanceMethod(classToUnswizzle, selector); + + NSCAssert(NULL != method, @"Selector %@ not found in %@ methods of class %@.", + NSStringFromSelector(selector), + class_isMetaClass(classToUnswizzle) ? @"class" : @"instance", classToUnswizzle); + + static pthread_mutex_t gLock = PTHREAD_MUTEX_INITIALIZER; + + pthread_mutex_lock(&gLock); + + IMP originalIMP = getRefToOriginalImplementation(key); + if (originalIMP) { + const char *methodType = method_getTypeEncoding(method); + class_replaceMethod(classToUnswizzle, selector, originalIMP, methodType); + + removeRefToOriginalImplementation(key); + } pthread_mutex_unlock(&gLock); } @@ -164,7 +229,7 @@ + (BOOL)swizzleInstanceMethod:(SEL)selector } } - swizzle(classToSwizzle, selector, factoryBlock); + swizzle(classToSwizzle, selector, factoryBlock, key); if (key) { [swizzledClassesForKey(key) addObject:classToSwizzle]; @@ -174,6 +239,29 @@ + (BOOL)swizzleInstanceMethod:(SEL)selector return YES; } ++ (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle key:(const void *)key +{ + NSAssert(key != NULL, @"Key may not be NULL."); + + if (key == NULL) { + NSLog(@"Key may not be NULL."); + return NO; + } + + @synchronized(swizzledClassesDictionary()) { + NSSet *swizzledClasses = swizzledClassesForKey(key); + if (![swizzledClasses containsObject:classToUnswizzle]) { + return NO; + } + + unswizzle(classToUnswizzle, selector, key); + + [swizzledClassesForKey(key) removeObject:classToUnswizzle]; + } + + return YES; +} + + (void)swizzleClassMethod:(SEL)selector inClass:(Class)classToSwizzle newImpFactory:(SentrySwizzleImpFactoryBlock)factoryBlock diff --git a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h index 1e21a23f6fe..995edb0699e 100644 --- a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h +++ b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h @@ -83,6 +83,18 @@ _SentrySWWrapArg(SentrySWArguments), _SentrySWWrapArg(SentrySWReplacement), \ SentrySwizzleMode, key) +/** + * Unswizzles the instance method of the class. + * + * @param classToUnswizzle The class with the method that should be unswizzled. + * + * @param selector Selector of the method that should be unswizzled. + * + * @return @c YES if successfully unswizzled and @c NO if the method was not swizzled. + */ +#define SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) \ + _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) + #pragma mark └ Swizzle Class Method /** @@ -302,6 +314,20 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { mode:(SentrySwizzleMode)mode key:(const void *)key; +/** + * Unswizzles the instance method of the class. + * + * @param selector Selector of the method that should be unswizzled. + * + * @param classToUnswizzle The class with the method that should be unswizzled. + * + * @param key The key is used in combination with the mode to indicate whether the + * swizzling should be done for the given class. + * + * @return @c YES if successfully unswizzled and @c NO if the method was not swizzled. + */ ++ (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle key:(const void *)key; + #pragma mark └ Swizzle Class method /** @@ -396,6 +422,9 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { mode:SentrySwizzleMode \ key:KEY]; +#define _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, KEY) \ + [SentrySwizzle unswizzleInstanceMethod:selector inClass:[classToUnswizzle class] key:KEY] + #define _SentrySwizzleClassMethod( \ classToSwizzle, selector, SentrySWReturnType, SentrySWArguments, SentrySWReplacement) \ [SentrySwizzle \ diff --git a/Tests/SentryTests/Helper/SentrySwizzleTests.m b/Tests/SentryTests/Helper/SentrySwizzleTests.m index b489027cfe6..635e7be4642 100644 --- a/Tests/SentryTests/Helper/SentrySwizzleTests.m +++ b/Tests/SentryTests/Helper/SentrySwizzleTests.m @@ -83,6 +83,10 @@ - (void)methodForSwizzlingWithoutCallOriginal { }; +- (void)methodForUnswizzling +{ +}; + - (NSString *)string { return @"ABC"; @@ -353,4 +357,23 @@ - (void)testSwizzleDontCallOriginalImplementation XCTAssertThrows([a methodForSwizzlingWithoutCallOriginal]); } +- (void)testUnswizzleInstanceMethod +{ + SEL methodForUnswizzling = NSSelectorFromString(@"methodForUnswizzling"); + + SentrySwizzleTestClass_A *object = [SentrySwizzleTestClass_B new]; + swizzleVoidMethod( + [SentrySwizzleTestClass_A class], methodForUnswizzling, ^{ SentryTestsLog(@"A"); }, + SentrySwizzleModeAlways, (void *)methodForUnswizzling); + [object methodForUnswizzling]; + ASSERT_LOG_IS(@"A"); + + [SentryTestsLog clear]; + + [SentrySwizzle unswizzleInstanceMethod:methodForUnswizzling + inClass:[SentrySwizzleTestClass_A class] + key:(void *)methodForUnswizzling]; + [object methodForUnswizzling]; + ASSERT_LOG_IS(@""); +} @end diff --git a/scripts/.clang-format-version b/scripts/.clang-format-version index a027c639c4c..87c0f53ffeb 100644 --- a/scripts/.clang-format-version +++ b/scripts/.clang-format-version @@ -1 +1 @@ -19.1.5 +19.1.6 From 60cb40f60c72554a0b15bcfb6bbede78260989c3 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 19 Dec 2024 10:59:03 +0100 Subject: [PATCH 2/9] add changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b95549b784..27849f95339 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ - `SentrySdkInfo.packages` should be an array (#4626) - Use the same SdkInfo for envelope header and event (#4629) -- Add method unswizzling +- Add method unswizzling (#4647) ### Internal From bdc6d9556cc7f9d1dae72233df40d56e053a2d2e Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 20 Dec 2024 11:01:07 +0100 Subject: [PATCH 3/9] change unswizzling to be available in test environment only --- Sources/Sentry/SentrySwizzle.m | 9 ++++++++- .../Sentry/include/HybridPublic/SentrySwizzle.h | 15 +++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Sources/Sentry/SentrySwizzle.m b/Sources/Sentry/SentrySwizzle.m index 6a37ee95ee9..c1d692da4c8 100644 --- a/Sources/Sentry/SentrySwizzle.m +++ b/Sources/Sentry/SentrySwizzle.m @@ -40,6 +40,7 @@ - (SentrySwizzleOriginalIMP)getOriginalImplementation @implementation SentrySwizzle +#if TEST || TESTCI static NSMutableDictionary * refsToOriginalImplementationsDictionary(void) { @@ -76,6 +77,7 @@ @implementation SentrySwizzle NSValue *originalImplementationValue = [refsToOriginalImplementations objectForKey:keyValue]; return (IMP)[originalImplementationValue pointerValue]; } +#endif // TEST || TESTCI static void swizzle( @@ -144,13 +146,16 @@ @implementation SentrySwizzle pthread_mutex_lock(&gLock); originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType); +#if TEST || TESTCI if (originalIMP) { storeRefToOriginalImplementation(key, originalIMP); } +#endif // TEST || TESTCI pthread_mutex_unlock(&gLock); } +#if TEST || TESTCI static void unswizzle(Class classToUnswizzle, SEL selector, const void *key) { @@ -174,7 +179,7 @@ @implementation SentrySwizzle pthread_mutex_unlock(&gLock); } - +#endif // TEST || TESTCI static NSMutableDictionary *> * swizzledClassesDictionary(void) { @@ -239,6 +244,7 @@ + (BOOL)swizzleInstanceMethod:(SEL)selector return YES; } +#if TEST || TESTCI + (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle key:(const void *)key { NSAssert(key != NULL, @"Key may not be NULL."); @@ -261,6 +267,7 @@ + (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle ke return YES; } +#endif // TEST || TESTCI + (void)swizzleClassMethod:(SEL)selector inClass:(Class)classToSwizzle diff --git a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h index 995edb0699e..19fbf845eca 100644 --- a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h +++ b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h @@ -83,6 +83,7 @@ _SentrySWWrapArg(SentrySWArguments), _SentrySWWrapArg(SentrySWReplacement), \ SentrySwizzleMode, key) +#if TEST || TESTCI /** * Unswizzles the instance method of the class. * @@ -92,8 +93,10 @@ * * @return @c YES if successfully unswizzled and @c NO if the method was not swizzled. */ -#define SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) \ - _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) +# define SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) \ + _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) + +#endif // TEST || TESTCI #pragma mark └ Swizzle Class Method @@ -314,6 +317,7 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { mode:(SentrySwizzleMode)mode key:(const void *)key; +#if TEST || TESTCI /** * Unswizzles the instance method of the class. * @@ -327,6 +331,7 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { * @return @c YES if successfully unswizzled and @c NO if the method was not swizzled. */ + (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle key:(const void *)key; +#endif // TEST || TESTCI #pragma mark └ Swizzle Class method @@ -422,8 +427,10 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { mode:SentrySwizzleMode \ key:KEY]; -#define _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, KEY) \ - [SentrySwizzle unswizzleInstanceMethod:selector inClass:[classToUnswizzle class] key:KEY] +#if TEST || TESTCI +# define _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, KEY) \ + [SentrySwizzle unswizzleInstanceMethod:selector inClass:[classToUnswizzle class] key:KEY] +#endif // TEST || TESTCI #define _SentrySwizzleClassMethod( \ classToSwizzle, selector, SentrySWReturnType, SentrySWArguments, SentrySWReplacement) \ From 8cff52ef251124950df91d0d60f4b0faee5290d1 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 15:59:56 +0100 Subject: [PATCH 4/9] remove merge conflict in changelog --- CHANGELOG.md | 43 +------------------------------------------ 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fa03da98e7..9a8d4387dfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,44 +2,10 @@ ## Unreleased -### Internal - -- Update to Xcode 16.2 in workflows (#4673) - -## 8.43.0 - -> [!WARNING] -> This release contains a breaking change for the previously experimental session replay options. We moved the options from Session from `options.experimental.sessionReplay` to `options.sessionReplay`. - -### Features - -- Session replay GA (#4662) -- Show session replay options as replay tags (#4639) - -### Fixes - -- Remove empty session replay tags (#4667) -- - `SentrySdkInfo.packages` should be an array (#4626) -- Use the same SdkInfo for envelope header and event (#4629) - -### Improvements - -- Improve compiler error message for missing Swift declarations due to APPLICATION_EXTENSION_API_ONLY (#4603) -- Mask screenshots for errors (#4623) -- Slightly speed up serializing scope (#4661) - -### Internal - -- Remove loading `integrations` names from `event.extra` (#4627) -- Add Hybrid SDKs API to add extra SDK packages (#4637) - -## 8.43.0-beta.1 - ### Improvements - Improve compiler error message for missing Swift declarations due to APPLICATION_EXTENSION_API_ONLY (#4603) - Mask screenshots for errors (#4623) -- Slightly speed up serializing scope (#4661) ### Features @@ -49,22 +15,15 @@ - `SentrySdkInfo.packages` should be an array (#4626) - Use the same SdkInfo for envelope header and event (#4629) -- Add method unswizzling (#4647) - Fixes Session replay screenshot provider crash (#4649) - Session Replay wrong clipping order (#4651) +- Add method unswizzling (#4647) ### Internal - Remove loading `integrations` names from `event.extra` (#4627) - Add Hybrid SDKs API to add extra SDK packages (#4637) -## 8.42.1 - -### Fixes - -- Fixes Session replay screenshot provider crash (#4649) -- Session Replay wrong clipping order (#4651) - ## 8.42.0 ### Features From 649105ee63502c8f6405233a9d08862884a69d99 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 16:37:49 +0100 Subject: [PATCH 5/9] add more documentation, null handling and test cases --- Sources/Sentry/SentrySwizzle.m | 72 +++++++++++++++++-- .../include/HybridPublic/SentrySwizzle.h | 5 ++ Tests/SentryTests/Helper/SentrySwizzleTests.m | 63 +++++++++++++++- 3 files changed, 132 insertions(+), 8 deletions(-) diff --git a/Sources/Sentry/SentrySwizzle.m b/Sources/Sentry/SentrySwizzle.m index c1d692da4c8..c1ee3bf47e6 100644 --- a/Sources/Sentry/SentrySwizzle.m +++ b/Sources/Sentry/SentrySwizzle.m @@ -1,4 +1,5 @@ #import "SentrySwizzle.h" +#import "SentryLog.h" #import #include @@ -40,7 +41,14 @@ - (SentrySwizzleOriginalIMP)getOriginalImplementation @implementation SentrySwizzle +// This lock is shared by all swizzling and unswizzling calls to ensure that +// only one thread is modifying the class at a time. +static pthread_mutex_t gLock = PTHREAD_MUTEX_INITIALIZER; + #if TEST || TESTCI +/** + * - Returns: a dictionary that maps keys to the references to the original implementations. + */ static NSMutableDictionary * refsToOriginalImplementationsDictionary(void) { @@ -50,31 +58,73 @@ @implementation SentrySwizzle return refsToOriginalImplementations; } +/** + * Adds a reference to the original implementation to the dictionary. + * + * If the key is NULL, it will log an error and NOT store the reference. + * + * - Parameter key: The key for which to store the reference to the original implementation. + * - Parameter implementation: Reference to the original implementation to store. + */ static void storeRefToOriginalImplementation(const void *key, IMP implementation) { + NSCAssert(key != NULL, @"Key may not be NULL."); + if (key == NULL) { + SENTRY_LOG_ERROR(@"Key may not be NULL."); + return; + } NSMutableDictionary *refsToOriginalImplementations = refsToOriginalImplementationsDictionary(); NSValue *keyValue = [NSValue valueWithPointer:key]; refsToOriginalImplementations[keyValue] = [NSValue valueWithPointer:implementation]; } +/** + * Removes a reference to the original implementation from the dictionary. + * + * If the key is NULL, it will log an error and do nothing. + * + * - Parameter key: The key for which to remove the reference to the original implementation. + */ static void removeRefToOriginalImplementation(const void *key) { + NSCAssert(key != NULL, @"Key may not be NULL."); + if (key == NULL) { + SENTRY_LOG_ERROR(@"Key may not be NULL."); + return; + } NSMutableDictionary *refsToOriginalImplementations = refsToOriginalImplementationsDictionary(); NSValue *keyValue = [NSValue valueWithPointer:key]; [refsToOriginalImplementations removeObjectForKey:keyValue]; } +/** + * Returns the original implementation for the given key. + * + * If the key is NULL, it will log an error and return NULL. + * If no original implementation is found, it will return NULL. + * + * - Parameter key: The key for which to get the original implementation. + * - Returns: The original implementation for the given key. + */ static IMP getRefToOriginalImplementation(const void *key) { + NSCAssert(key != NULL, @"Key may not be NULL."); + if (key == NULL) { + SENTRY_LOG_ERROR(@"Key may not be NULL."); + return NULL; + } NSMutableDictionary *refsToOriginalImplementations = refsToOriginalImplementationsDictionary(); NSValue *keyValue = [NSValue valueWithPointer:key]; NSValue *originalImplementationValue = [refsToOriginalImplementations objectForKey:keyValue]; + if (originalImplementationValue == nil) { + return NULL; + } return (IMP)[originalImplementationValue pointerValue]; } #endif // TEST || TESTCI @@ -89,8 +139,6 @@ @implementation SentrySwizzle NSStringFromSelector(selector), class_isMetaClass(classToSwizzle) ? @"class" : @"instance", classToSwizzle); - static pthread_mutex_t gLock = PTHREAD_MUTEX_INITIALIZER; - // To keep things thread-safe, we fill in the originalIMP later, // with the result of the class_replaceMethod call below. __block IMP originalIMP = NULL; @@ -148,7 +196,12 @@ @implementation SentrySwizzle originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType); #if TEST || TESTCI if (originalIMP) { - storeRefToOriginalImplementation(key, originalIMP); + NSCAssert(key != NULL, @"Key may not be NULL."); + if (key != NULL) { + storeRefToOriginalImplementation(key, originalIMP); + } else { + SENTRY_LOG_WARN(@"Key may not be NULL."); + } } #endif // TEST || TESTCI @@ -159,14 +212,19 @@ @implementation SentrySwizzle static void unswizzle(Class classToUnswizzle, SEL selector, const void *key) { + NSCAssert(key != NULL, @"Key may not be NULL."); + + if (key == NULL) { + SENTRY_LOG_WARN(@"Key may not be NULL."); + return; + } + Method method = class_getInstanceMethod(classToUnswizzle, selector); NSCAssert(NULL != method, @"Selector %@ not found in %@ methods of class %@.", NSStringFromSelector(selector), class_isMetaClass(classToUnswizzle) ? @"class" : @"instance", classToUnswizzle); - static pthread_mutex_t gLock = PTHREAD_MUTEX_INITIALIZER; - pthread_mutex_lock(&gLock); IMP originalIMP = getRefToOriginalImplementation(key); @@ -213,7 +271,7 @@ + (BOOL)swizzleInstanceMethod:(SEL)selector @"Key may not be NULL if mode is not SentrySwizzleModeAlways."); if (key == NULL && mode != SentrySwizzleModeAlways) { - NSLog(@"Key may not be NULL if mode is not SentrySwizzleModeAlways."); + SENTRY_LOG_WARN(@"Key may not be NULL if mode is not SentrySwizzleModeAlways."); return NO; } @@ -250,7 +308,7 @@ + (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle ke NSAssert(key != NULL, @"Key may not be NULL."); if (key == NULL) { - NSLog(@"Key may not be NULL."); + SENTRY_LOG_WARN(@"Key may not be NULL."); return NO; } diff --git a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h index 19fbf845eca..0493a96ba50 100644 --- a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h +++ b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h @@ -86,6 +86,8 @@ #if TEST || TESTCI /** * Unswizzles the instance method of the class. + * To reduce the risk of breaking functionality with unswizzling, this method is not considered + * safe-to-use in production and only available in test targets. * * @param classToUnswizzle The class with the method that should be unswizzled. * @@ -321,6 +323,9 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { /** * Unswizzles the instance method of the class. * + * To reduce the risk of breaking functionality with unswizzling, this method is not considered + * safe-to-use in production and only available in test targets. + * * @param selector Selector of the method that should be unswizzled. * * @param classToUnswizzle The class with the method that should be unswizzled. diff --git a/Tests/SentryTests/Helper/SentrySwizzleTests.m b/Tests/SentryTests/Helper/SentrySwizzleTests.m index 635e7be4642..882ab2dbd74 100644 --- a/Tests/SentryTests/Helper/SentrySwizzleTests.m +++ b/Tests/SentryTests/Helper/SentrySwizzleTests.m @@ -359,21 +359,82 @@ - (void)testSwizzleDontCallOriginalImplementation - (void)testUnswizzleInstanceMethod { + // -- Arrange -- SEL methodForUnswizzling = NSSelectorFromString(@"methodForUnswizzling"); - SentrySwizzleTestClass_A *object = [SentrySwizzleTestClass_B new]; + + // Swizzle the method once swizzleVoidMethod( [SentrySwizzleTestClass_A class], methodForUnswizzling, ^{ SentryTestsLog(@"A"); }, SentrySwizzleModeAlways, (void *)methodForUnswizzling); + + // Smoke test the swizzling [object methodForUnswizzling]; ASSERT_LOG_IS(@"A"); + [SentryTestsLog clear]; + + // -- Act -- + [SentrySwizzle unswizzleInstanceMethod:methodForUnswizzling + inClass:[SentrySwizzleTestClass_A class] + key:(void *)methodForUnswizzling]; + [object methodForUnswizzling]; + + // -- Assert -- + ASSERT_LOG_IS(@""); +} +- (void)testUnswizzleInstanceMethod_methodNotSwizzled_shouldWork +{ + // -- Arrange -- + SEL methodForUnswizzling = NSSelectorFromString(@"methodForUnswizzling"); + SentrySwizzleTestClass_A *object = [SentrySwizzleTestClass_A new]; + + // Smoke-test the swizzling + [object methodForUnswizzling]; + ASSERT_LOG_IS(@"A"); [SentryTestsLog clear]; + // -- Act -- [SentrySwizzle unswizzleInstanceMethod:methodForUnswizzling inClass:[SentrySwizzleTestClass_A class] key:(void *)methodForUnswizzling]; [object methodForUnswizzling]; + + // -- Assert -- ASSERT_LOG_IS(@""); } + +- (void)testUnswizzleInstanceMethod_unswizzlingMethodMultipleTimes_shouldWork +{ + // -- Arrange -- + SEL methodForUnswizzling = NSSelectorFromString(@"methodForUnswizzling"); + SentrySwizzleTestClass_A *object = [SentrySwizzleTestClass_A new]; + + swizzleVoidMethod( + [SentrySwizzleTestClass_A class], methodForUnswizzling, ^{ SentryTestsLog(@"A"); }, + SentrySwizzleModeAlways, (void *)methodForUnswizzling); + + // Smoke test the swizzling + [object methodForUnswizzling]; + ASSERT_LOG_IS(@"A"); + [SentryTestsLog clear]; + + [SentrySwizzle unswizzleInstanceMethod:methodForUnswizzling + inClass:[SentrySwizzleTestClass_A class] + key:(void *)methodForUnswizzling]; + [object methodForUnswizzling]; + ASSERT_LOG_IS(@""); + [SentryTestsLog clear]; + + // -- Act -- + // Unswizzle again should not cause issues + [SentrySwizzle unswizzleInstanceMethod:methodForUnswizzling + inClass:[SentrySwizzleTestClass_A class] + key:(void *)methodForUnswizzling]; + [object methodForUnswizzling]; + + // -- Assert - + ASSERT_LOG_IS(@""); +} + @end From 55231ef0c04008bb60dc38747e77a619bfd60f4e Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 16:43:04 +0100 Subject: [PATCH 6/9] replace NSLog with SentryLog --- Sources/Sentry/SentrySwizzle.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentrySwizzle.m b/Sources/Sentry/SentrySwizzle.m index c1ee3bf47e6..93895568972 100644 --- a/Sources/Sentry/SentrySwizzle.m +++ b/Sources/Sentry/SentrySwizzle.m @@ -21,7 +21,7 @@ - (SentrySwizzleOriginalIMP)getOriginalImplementation { NSAssert(_impProviderBlock, @"_impProviderBlock can't be missing"); if (!_impProviderBlock) { - NSLog(@"_impProviderBlock can't be missing"); + SENTRY_LOG_ERROR(@"_impProviderBlock can't be missing"); return NULL; } From 451caaee9cebee2c67ca6ecb7132bc5ea875658a Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 16:49:56 +0100 Subject: [PATCH 7/9] add workflow dispatch for testflight upload --- .github/workflows/testflight.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index fbc46498492..b17283b8978 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -14,6 +14,7 @@ on: pull_request: paths: - '.github/workflows/testflight.yml' + workflow_dispatch: jobs: upload_to_testflight: From 4bc7b322065aa9a0fefa869b8b85621d0a1a3320 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 16:51:12 +0100 Subject: [PATCH 8/9] remove workflow dispatch --- .github/workflows/testflight.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index b17283b8978..fbc46498492 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -14,7 +14,6 @@ on: pull_request: paths: - '.github/workflows/testflight.yml' - workflow_dispatch: jobs: upload_to_testflight: From 4dc0029a6f5a7bc5d308c4b1afab9b39f2d02f65 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Wed, 8 Jan 2025 11:47:41 +0100 Subject: [PATCH 9/9] fix issues and merge conflicts --- CHANGELOG.md | 45 +++++++++++++++++-- Sources/Sentry/SentrySwizzle.m | 7 ++- .../include/HybridPublic/SentrySwizzle.h | 25 +++++++---- Tests/SentryTests/Helper/SentrySwizzleTests.m | 2 +- 4 files changed, 63 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a8d4387dfd..588e150a99a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,45 @@ ## Unreleased +### Internal + +- Update to Xcode 16.2 in workflows (#4673) +- Add method unswizzling (#4647) + +## 8.43.0 + +> [!WARNING] +> This release contains a breaking change for the previously experimental session replay options. We moved the options from Session from `options.experimental.sessionReplay` to `options.sessionReplay`. + +### Features + +- Session replay GA (#4662) +- Show session replay options as replay tags (#4639) + +### Fixes + +- Remove empty session replay tags (#4667) +- - `SentrySdkInfo.packages` should be an array (#4626) +- Use the same SdkInfo for envelope header and event (#4629) + +### Improvements + +- Improve compiler error message for missing Swift declarations due to APPLICATION_EXTENSION_API_ONLY (#4603) +- Mask screenshots for errors (#4623) +- Slightly speed up serializing scope (#4661) + +### Internal + +- Remove loading `integrations` names from `event.extra` (#4627) +- Add Hybrid SDKs API to add extra SDK packages (#4637) + +## 8.43.0-beta.1 + ### Improvements - Improve compiler error message for missing Swift declarations due to APPLICATION_EXTENSION_API_ONLY (#4603) - Mask screenshots for errors (#4623) +- Slightly speed up serializing scope (#4661) ### Features @@ -15,15 +50,19 @@ - `SentrySdkInfo.packages` should be an array (#4626) - Use the same SdkInfo for envelope header and event (#4629) -- Fixes Session replay screenshot provider crash (#4649) -- Session Replay wrong clipping order (#4651) -- Add method unswizzling (#4647) ### Internal - Remove loading `integrations` names from `event.extra` (#4627) - Add Hybrid SDKs API to add extra SDK packages (#4637) +## 8.42.1 + +### Fixes + +- Fixes Session replay screenshot provider crash (#4649) +- Session Replay wrong clipping order (#4651) + ## 8.42.0 ### Features diff --git a/Sources/Sentry/SentrySwizzle.m b/Sources/Sentry/SentrySwizzle.m index 93895568972..c8f10fb8e95 100644 --- a/Sources/Sentry/SentrySwizzle.m +++ b/Sources/Sentry/SentrySwizzle.m @@ -196,11 +196,11 @@ @implementation SentrySwizzle originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType); #if TEST || TESTCI if (originalIMP) { - NSCAssert(key != NULL, @"Key may not be NULL."); if (key != NULL) { storeRefToOriginalImplementation(key, originalIMP); } else { - SENTRY_LOG_WARN(@"Key may not be NULL."); + SENTRY_LOG_WARN( + @"Swizzling without a key is not recommended, because they can not be unswizzled."); } } #endif // TEST || TESTCI @@ -213,7 +213,6 @@ @implementation SentrySwizzle unswizzle(Class classToUnswizzle, SEL selector, const void *key) { NSCAssert(key != NULL, @"Key may not be NULL."); - if (key == NULL) { SENTRY_LOG_WARN(@"Key may not be NULL."); return; @@ -238,6 +237,7 @@ @implementation SentrySwizzle pthread_mutex_unlock(&gLock); } #endif // TEST || TESTCI + static NSMutableDictionary *> * swizzledClassesDictionary(void) { @@ -306,7 +306,6 @@ + (BOOL)swizzleInstanceMethod:(SEL)selector + (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle key:(const void *)key { NSAssert(key != NULL, @"Key may not be NULL."); - if (key == NULL) { SENTRY_LOG_WARN(@"Key may not be NULL."); return NO; diff --git a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h index 0493a96ba50..3b789d1b5a7 100644 --- a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h +++ b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h @@ -86,18 +86,18 @@ #if TEST || TESTCI /** * Unswizzles the instance method of the class. - * To reduce the risk of breaking functionality with unswizzling, this method is not considered - * safe-to-use in production and only available in test targets. * - * @param classToUnswizzle The class with the method that should be unswizzled. + * @warning To reduce the risk of breaking functionality with unswizzling, this method is not + * considered safe-to-use in production and only available in test targets. * + * @param classToUnswizzle The class with the method that should be unswizzled. * @param selector Selector of the method that should be unswizzled. + * @param key The key to unswizzle the method with. * * @return @c YES if successfully unswizzled and @c NO if the method was not swizzled. */ # define SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) \ _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) - #endif // TEST || TESTCI #pragma mark └ Swizzle Class Method @@ -323,13 +323,11 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { /** * Unswizzles the instance method of the class. * - * To reduce the risk of breaking functionality with unswizzling, this method is not considered - * safe-to-use in production and only available in test targets. + * @warning To reduce the risk of breaking functionality with unswizzling, this method is not + * considered safe-to-use in production and only available in test targets. * * @param selector Selector of the method that should be unswizzled. - * * @param classToUnswizzle The class with the method that should be unswizzled. - * * @param key The key is used in combination with the mode to indicate whether the * swizzling should be done for the given class. * @@ -433,6 +431,17 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { key:KEY]; #if TEST || TESTCI +/** + * Macro to unswizzle an instance method. + * + * @warning To reduce the risk of breaking functionality with unswizzling, this macro is not + * considered safe-to-use in production and only available in test targets. + * + * @param classToUnswizzle The class to unswizzle the method from. + * @param selector The selector of the method to unswizzle. + * @param KEY The key to unswizzle the method with. + * @return @c YES if the method was successfully unswizzled, @c NO otherwise. + */ # define _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, KEY) \ [SentrySwizzle unswizzleInstanceMethod:selector inClass:[classToUnswizzle class] key:KEY] #endif // TEST || TESTCI diff --git a/Tests/SentryTests/Helper/SentrySwizzleTests.m b/Tests/SentryTests/Helper/SentrySwizzleTests.m index 882ab2dbd74..ec30b5f44f9 100644 --- a/Tests/SentryTests/Helper/SentrySwizzleTests.m +++ b/Tests/SentryTests/Helper/SentrySwizzleTests.m @@ -391,7 +391,7 @@ - (void)testUnswizzleInstanceMethod_methodNotSwizzled_shouldWork // Smoke-test the swizzling [object methodForUnswizzling]; - ASSERT_LOG_IS(@"A"); + ASSERT_LOG_IS(@""); [SentryTestsLog clear]; // -- Act --