Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add method unswizzling #4647

Merged
merged 12 commits into from
Jan 8, 2025
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
### Internal

- Update to Xcode 16.2 in workflows (#4673)
- Add method unswizzling (#4647)

## 8.43.0

Expand Down
164 changes: 158 additions & 6 deletions Sources/Sentry/SentrySwizzle.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#import "SentrySwizzle.h"
#import "SentryLog.h"

#import <objc/runtime.h>
#include <pthread.h>
Expand All @@ -20,7 +21,7 @@
{
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;
}

Expand All @@ -40,17 +41,104 @@

@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<NSValue *, NSValue *> *
refsToOriginalImplementationsDictionary(void)
{
static NSMutableDictionary *refsToOriginalImplementations;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ refsToOriginalImplementations = [NSMutableDictionary new]; });
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.
*/
philprime marked this conversation as resolved.
Show resolved Hide resolved
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;

Check warning on line 75 in Sources/Sentry/SentrySwizzle.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySwizzle.m#L75

Added line #L75 was not covered by tests
}
NSMutableDictionary<NSValue *, NSValue *> *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;

Check warning on line 96 in Sources/Sentry/SentrySwizzle.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySwizzle.m#L96

Added line #L96 was not covered by tests
}
NSMutableDictionary<NSValue *, NSValue *> *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;

Check warning on line 119 in Sources/Sentry/SentrySwizzle.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySwizzle.m#L119

Added line #L119 was not covered by tests
}
NSMutableDictionary<NSValue *, NSValue *> *refsToOriginalImplementations
= refsToOriginalImplementationsDictionary();
NSValue *keyValue = [NSValue valueWithPointer:key];
NSValue *originalImplementationValue = [refsToOriginalImplementations objectForKey:keyValue];
if (originalImplementationValue == nil) {
return NULL;

Check warning on line 126 in Sources/Sentry/SentrySwizzle.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySwizzle.m#L126

Added line #L126 was not covered by tests
}
return (IMP)[originalImplementationValue pointerValue];
}
#endif // TEST || TESTCI

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);

NSCAssert(NULL != method, @"Selector %@ not found in %@ methods of class %@.",
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;
Expand Down Expand Up @@ -106,10 +194,50 @@
pthread_mutex_lock(&gLock);

originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType);
#if TEST || TESTCI
if (originalIMP) {
if (key != NULL) {
storeRefToOriginalImplementation(key, originalIMP);
} else {
SENTRY_LOG_WARN(
@"Swizzling without a key is not recommended, because they can not be unswizzled.");
}
}
#endif // TEST || TESTCI

pthread_mutex_unlock(&gLock);
}

#if TEST || TESTCI
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;

Check warning on line 218 in Sources/Sentry/SentrySwizzle.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySwizzle.m#L218

Added line #L218 was not covered by tests
}

Method method = class_getInstanceMethod(classToUnswizzle, selector);

NSCAssert(NULL != method, @"Selector %@ not found in %@ methods of class %@.",
NSStringFromSelector(selector),
class_isMetaClass(classToUnswizzle) ? @"class" : @"instance", classToUnswizzle);

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);
}
#endif // TEST || TESTCI

static NSMutableDictionary<NSValue *, NSMutableSet<Class> *> *
swizzledClassesDictionary(void)
{
Expand Down Expand Up @@ -143,7 +271,7 @@
@"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;
}

Expand All @@ -164,7 +292,7 @@
}
}

swizzle(classToSwizzle, selector, factoryBlock);
swizzle(classToSwizzle, selector, factoryBlock, key);
philprime marked this conversation as resolved.
Show resolved Hide resolved

if (key) {
[swizzledClassesForKey(key) addObject:classToSwizzle];
Expand All @@ -174,6 +302,30 @@
return YES;
}

#if TEST || TESTCI
+ (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;

Check warning on line 311 in Sources/Sentry/SentrySwizzle.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySwizzle.m#L311

Added line #L311 was not covered by tests
}

@synchronized(swizzledClassesDictionary()) {
NSSet<Class> *swizzledClasses = swizzledClassesForKey(key);
if (![swizzledClasses containsObject:classToUnswizzle]) {
return NO;
}

unswizzle(classToUnswizzle, selector, key);

[swizzledClassesForKey(key) removeObject:classToUnswizzle];
}

return YES;
}
#endif // TEST || TESTCI

+ (void)swizzleClassMethod:(SEL)selector
inClass:(Class)classToSwizzle
newImpFactory:(SentrySwizzleImpFactoryBlock)factoryBlock
Expand Down
50 changes: 50 additions & 0 deletions Sources/Sentry/include/HybridPublic/SentrySwizzle.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,23 @@
_SentrySWWrapArg(SentrySWArguments), _SentrySWWrapArg(SentrySWReplacement), \
SentrySwizzleMode, key)

#if TEST || TESTCI
/**
* Unswizzles the instance method of the class.
philprime marked this conversation as resolved.
Show resolved Hide resolved
*
* @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

/**
Expand Down Expand Up @@ -302,6 +319,23 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) {
mode:(SentrySwizzleMode)mode
key:(const void *)key;

#if TEST || TESTCI
/**
* Unswizzles the instance method of the class.
*
* @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.
*
* @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

/**
Expand Down Expand Up @@ -396,6 +430,22 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) {
mode: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

#define _SentrySwizzleClassMethod( \
classToSwizzle, selector, SentrySWReturnType, SentrySWArguments, SentrySWReplacement) \
[SentrySwizzle \
Expand Down
84 changes: 84 additions & 0 deletions Tests/SentryTests/Helper/SentrySwizzleTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ - (void)methodForSwizzlingWithoutCallOriginal
{
};

- (void)methodForUnswizzling
{
};

- (NSString *)string
{
return @"ABC";
Expand Down Expand Up @@ -353,4 +357,84 @@ - (void)testSwizzleDontCallOriginalImplementation
XCTAssertThrows([a methodForSwizzlingWithoutCallOriginal]);
}

- (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(@"");
[SentryTestsLog clear];

// -- Act --
[SentrySwizzle unswizzleInstanceMethod:methodForUnswizzling
philprime marked this conversation as resolved.
Show resolved Hide resolved
philprime marked this conversation as resolved.
Show resolved Hide resolved
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
Loading