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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
44 changes: 3 additions & 41 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -49,19 +15,15 @@

- `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
Expand Down
165 changes: 159 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 @@ - (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;
}

Expand All @@ -40,17 +41,104 @@ - (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<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.
*/
static void
swizzle(Class classToSwizzle, SEL selector, SentrySwizzleImpFactoryBlock factoryBlock)
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<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;
}
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;
}
NSMutableDictionary<NSValue *, NSValue *> *refsToOriginalImplementations
= refsToOriginalImplementationsDictionary();
NSValue *keyValue = [NSValue valueWithPointer:key];
NSValue *originalImplementationValue = [refsToOriginalImplementations objectForKey:keyValue];
if (originalImplementationValue == nil) {
return NULL;
}
return (IMP)[originalImplementationValue pointerValue];
}
#endif // TEST || TESTCI

static void
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 @@ @implementation SentrySwizzle
pthread_mutex_lock(&gLock);

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.");
}
}
#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;
}

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 @@ + (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;
}

Expand All @@ -164,7 +292,7 @@ + (BOOL)swizzleInstanceMethod:(SEL)selector
}
}

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,31 @@ + (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.");

if (key == NULL) {
SENTRY_LOG_WARN(@"Key may not be NULL.");
return NO;
}

@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
41 changes: 41 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
* 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.
*
* @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,25 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) {
mode:(SentrySwizzleMode)mode
key:(const void *)key;

#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 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 +432,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
Loading
Loading