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 @@ -15,6 +15,7 @@

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

Expand Down
99 changes: 97 additions & 2 deletions Sources/Sentry/SentrySwizzle.m
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,48 @@

@implementation SentrySwizzle

#if TEST || TESTCI
static NSMutableDictionary<NSValue *, NSValue *> *
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<NSValue *, NSValue *> *refsToOriginalImplementations
= refsToOriginalImplementationsDictionary();
NSValue *keyValue = [NSValue valueWithPointer:key];
refsToOriginalImplementations[keyValue] = [NSValue valueWithPointer:implementation];
}

static void
removeRefToOriginalImplementation(const void *key)
{
NSMutableDictionary<NSValue *, NSValue *> *refsToOriginalImplementations
= refsToOriginalImplementationsDictionary();
NSValue *keyValue = [NSValue valueWithPointer:key];
[refsToOriginalImplementations removeObjectForKey:keyValue];
}

static IMP
getRefToOriginalImplementation(const void *key)
{
NSMutableDictionary<NSValue *, NSValue *> *refsToOriginalImplementations
= refsToOriginalImplementationsDictionary();
NSValue *keyValue = [NSValue valueWithPointer:key];
NSValue *originalImplementationValue = [refsToOriginalImplementations objectForKey:keyValue];
return (IMP)[originalImplementationValue pointerValue];
philprime marked this conversation as resolved.
Show resolved Hide resolved
}
#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);

Expand Down Expand Up @@ -106,10 +146,40 @@
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)
{
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;
philprime marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -164,7 +234,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 +244,31 @@
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) {
NSLog(@"Key may not be NULL.");
philprime marked this conversation as resolved.
Show resolved Hide resolved
return NO;

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

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySwizzle.m#L253-L254

Added lines #L253 - L254 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySwizzle.m#L260

Added line #L260 was not covered by tests
}

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
36 changes: 36 additions & 0 deletions Sources/Sentry/include/HybridPublic/SentrySwizzle.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@
_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
*
* @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)

#endif // TEST || TESTCI

#pragma mark └ Swizzle Class Method

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

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

#pragma mark └ Swizzle Class method

/**
Expand Down Expand Up @@ -396,6 +427,11 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) {
mode:SentrySwizzleMode \
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) \
[SentrySwizzle \
Expand Down
23 changes: 23 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,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
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_LOG_IS(@"");
}
@end
2 changes: 1 addition & 1 deletion scripts/.clang-format-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
19.1.5
19.1.6
Loading