From 34244b2fb314221b17b38398ae3ad36c7a297d48 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 18 Jun 2020 23:38:14 +0200 Subject: [PATCH 1/6] First implementation of Dynamic Invocation --- InterposeKit.xcodeproj/project.pbxproj | 12 +++ Sources/InterposeKit/DynamicHook.swift | 99 +++++++++++++++++++ Sources/InterposeKit/HookFinder.swift | 3 +- Sources/InterposeKit/InterposeKit.swift | 13 ++- Sources/InterposeKit/InterposeSubclass.swift | 90 +++++++++++++++++ Sources/InterposeKit/ObjCInvocation.swift | 37 +++++++ Sources/InterposeKit/ObjectHook.swift | 16 +-- Sources/SuperBuilder/src/ITKSuperBuilder.m | 26 ++++- .../InterposeKitTests/DynamicHookTests.swift | 33 +++++++ Tests/InterposeKitTests/TestClass.swift | 4 + 10 files changed, 313 insertions(+), 20 deletions(-) create mode 100644 Sources/InterposeKit/DynamicHook.swift create mode 100644 Sources/InterposeKit/ObjCInvocation.swift create mode 100644 Tests/InterposeKitTests/DynamicHookTests.swift diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 81865b4..d901102 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -17,6 +17,9 @@ 781095B4248D6DFD008A943C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 781095B2248D6DFD008A943C /* LaunchScreen.storyboard */; }; 781095F5248E7C91008A943C /* InterposeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; }; 781095F6248E7C91008A943C /* InterposeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 781EB556249BEF54002545B4 /* ObjCInvocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781EB555249BEF54002545B4 /* ObjCInvocation.swift */; }; + 781EB558249BF0D1002545B4 /* DynamicHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781EB557249BF0D1002545B4 /* DynamicHook.swift */; }; + 781EB55A249BFA58002545B4 /* DynamicHookTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781EB559249BFA58002545B4 /* DynamicHookTests.swift */; }; 78A2F265249635B100F5AC5F /* KVOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F264249635B100F5AC5F /* KVOTests.swift */; }; 78A2F26724964AF200F5AC5F /* InterposeKitTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */; }; 78A2F26E2496B54B00F5AC5F /* InterposeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26D2496B54B00F5AC5F /* InterposeError.swift */; }; @@ -78,6 +81,9 @@ 781095B3248D6DFD008A943C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 781095B5248D6DFD008A943C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 781095B9248D6E0A008A943C /* InterposeTestHost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = InterposeTestHost.entitlements; sourceTree = ""; }; + 781EB555249BEF54002545B4 /* ObjCInvocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ObjCInvocation.swift; path = Sources/InterposeKit/ObjCInvocation.swift; sourceTree = SOURCE_ROOT; }; + 781EB557249BF0D1002545B4 /* DynamicHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DynamicHook.swift; path = Sources/InterposeKit/DynamicHook.swift; sourceTree = SOURCE_ROOT; }; + 781EB559249BFA58002545B4 /* DynamicHookTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DynamicHookTests.swift; path = Tests/InterposeKitTests/DynamicHookTests.swift; sourceTree = SOURCE_ROOT; }; 78863EC62464B2F900BA3762 /* InterposeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = InterposeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 78863ECA2464B2F900BA3762 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = InterposeKit.xcodeproj/Info.plist; sourceTree = ""; }; 78A2F264249635B100F5AC5F /* KVOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KVOTests.swift; path = Tests/InterposeKitTests/KVOTests.swift; sourceTree = SOURCE_ROOT; }; @@ -184,10 +190,12 @@ 78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */, 7810959D248D43DC008A943C /* ClassHook.swift */, 78EDB8FE248D0A9900D2F6C1 /* ObjectHook.swift */, + 781EB557249BF0D1002545B4 /* DynamicHook.swift */, 78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */, 7810959F248D50C1008A943C /* Watcher.swift */, 78E20D9724981B2A0021552C /* InterposeSubclass.swift */, 780FC9F9249822C900DA5A14 /* HookFinder.swift */, + 781EB555249BEF54002545B4 /* ObjCInvocation.swift */, ); path = InterposeKit; sourceTree = ""; @@ -208,6 +216,7 @@ 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */, 78C39D922483169300B46395 /* InterposeKitTests.swift */, 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */, + 781EB559249BFA58002545B4 /* DynamicHookTests.swift */, 78C5A4A72494D75100EE9756 /* MultipleInterposing.swift */, 78A2F264249635B100F5AC5F /* KVOTests.swift */, 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */, @@ -405,7 +414,9 @@ 78C39D912483165600B46395 /* InterposeKit.swift in Sources */, 78EDB8FF248D0A9900D2F6C1 /* ObjectHook.swift in Sources */, 78EDB903248D42CD00D2F6C1 /* LinuxCompileSupport.swift in Sources */, + 781EB556249BEF54002545B4 /* ObjCInvocation.swift in Sources */, 78E20D9824981B2A0021552C /* InterposeSubclass.swift in Sources */, + 781EB558249BF0D1002545B4 /* DynamicHook.swift in Sources */, 78EDB8DD248BAA5600D2F6C1 /* AnyHook.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -418,6 +429,7 @@ 78C5A4A82494D75100EE9756 /* MultipleInterposing.swift in Sources */, 78EDB8DA248BA9B300D2F6C1 /* TestClass.swift in Sources */, 78EDB8DB248BA9BB00D2F6C1 /* ObjectInterposeTests.swift in Sources */, + 781EB55A249BFA58002545B4 /* DynamicHookTests.swift in Sources */, 78A2F26724964AF200F5AC5F /* InterposeKitTestCase.swift in Sources */, 78A2F265249635B100F5AC5F /* KVOTests.swift in Sources */, ); diff --git a/Sources/InterposeKit/DynamicHook.swift b/Sources/InterposeKit/DynamicHook.swift new file mode 100644 index 0000000..d66a66e --- /dev/null +++ b/Sources/InterposeKit/DynamicHook.swift @@ -0,0 +1,99 @@ +import Foundation + +extension Interpose { + + /// Hook that uses `NSInvocation `to not require specific signatures + /// The call is converted into an invocation via `_objc_msgForward`. + final public class DynamicHook: AnyHook { + + /// The object that is being hooked. + public let object: AnyObject + + /// The position of this hook. + public let strategy: AspectStrategy + + /// The stored action to be called + public let action: (AnyObject) -> Void + + /// Subclass that we create on the fly + var interposeSubclass: InterposeSubclass? + + public init(object: AnyObject, selector: Selector, + strategy: AspectStrategy = .before, + implementation: @escaping (AnyObject) -> Void) throws { + self.object = object + self.strategy = strategy + self.action = implementation + try super.init(class: type(of: object), selector: selector) + } + + private lazy var forwardIMP: IMP = { + let imp = dlsym(dlopen(nil, RTLD_LAZY), "_objc_msgForward") + return unsafeBitCast(imp, to: IMP.self) + }() + + override func replaceImplementation() throws { + let method = try validate() + let encoding = method_getTypeEncoding(method) + + // Check if there's an existing subclass we can reuse. + // Create one at runtime if there is none. + let subclass = try InterposeSubclass(object: object) + try subclass.prepareDynamicInvocation() + interposeSubclass = subclass + + let hasExistingMethod = subclass.exactClassImplementsSelector(selector) + if !hasExistingMethod { + // Add super trampoline, then swizzle + subclass.addSuperTrampoline(selector: selector) + let superCallingMethod = class_getInstanceMethod(subclass.dynamicClass, selector)! + + let aspectSelector = InterposeSubclass.aspectPrefixed(selector) + let origImp = method_getImplementation(superCallingMethod) + class_addMethod(subclass.dynamicClass, aspectSelector, origImp, encoding) + Interpose.log("Generated -[\(`class`).\(aspectSelector)]: \(origImp)") + } + + // append hook as copy + let newContainer = DynamicHookContainer() + var hooks = subclass.hookContainer?.hooks ?? [] + hooks.append(self) + newContainer.hooks = hooks + subclass.hookContainer = newContainer + + let forwardIMP = self.forwardIMP + guard class_replaceMethod(subclass.dynamicClass, selector, forwardIMP, encoding) != nil else { + throw InterposeError.unableToAddMethod(subclass.dynamicClass, selector) + } + + Interpose.log("Added dynamic -[\(`class`).\(selector)]") + } + + override func resetImplementation() throws { + //let method = try validate(expectedState: .interposed) + + // TODO + } + } + + /// Store all hooks + class DynamicHookContainer { + var hooks: [DynamicHook] = [] + + var before: [DynamicHook] { + hooks.filter { $0.strategy == .before } + } + var instead: [DynamicHook] { + hooks.filter { $0.strategy == .instead } + } + var after: [DynamicHook] { + hooks.filter { $0.strategy == .after } + } + } +} + +extension Collection where Iterator.Element == Interpose.DynamicHook { + func executeAll(_ bSelf: AnyObject) { + forEach { $0.action(bSelf) } + } +} diff --git a/Sources/InterposeKit/HookFinder.swift b/Sources/InterposeKit/HookFinder.swift index fccf06f..30ce805 100644 --- a/Sources/InterposeKit/HookFinder.swift +++ b/Sources/InterposeKit/HookFinder.swift @@ -2,8 +2,9 @@ import Foundation extension Interpose { - private struct AssociatedKeys { + struct AssociatedKeys { static var hookForBlock: UInt8 = 0 + static var hookContainer: UInt8 = 0 } private class WeakObjectContainer: NSObject { diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index 24c2a61..012f240 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -15,6 +15,15 @@ extension NSObject { } } + /// Hook an `@objc dynamic` instance method via selector on the current object or class.. + @discardableResult public func hook ( + _ selector: Selector, + strategy: AspectStrategy = .before, + _ implementation: @escaping (AnyObject) -> Void) throws -> AnyHook { + return try Interpose.DynamicHook(object: self, selector: selector, + strategy: strategy, implementation: implementation).apply() + } + /// Hook an `@objc dynamic` instance method via selector on the current object or class.. @discardableResult public class func hook ( _ selector: Selector, @@ -97,8 +106,8 @@ final public class Interpose { hookSignature: HookSignature.Type = HookSignature.self, _ implementation: (TypedHook) -> HookSignature?) throws -> TypedHook { - try hook(NSSelectorFromString(selName), - methodSignature: methodSignature, hookSignature: hookSignature, implementation) + try hook(NSSelectorFromString(selName), + methodSignature: methodSignature, hookSignature: hookSignature, implementation) } /// Hook an `@objc dynamic` instance method via selector on the current class. diff --git a/Sources/InterposeKit/InterposeSubclass.swift b/Sources/InterposeKit/InterposeSubclass.swift index 7752ce1..441d5f5 100644 --- a/Sources/InterposeKit/InterposeSubclass.swift +++ b/Sources/InterposeKit/InterposeSubclass.swift @@ -8,10 +8,14 @@ class InterposeSubclass { enum ObjCSelector { static let getClass = Selector((("class"))) + static let forwardInvocation = Selector((("forwardInvocation:"))) + //static let methodSignatureForSelector = Selector((("methodSignatureForSelector:"))) } enum ObjCMethodEncoding { static let getClass = extract("#@:") + static let forwardInvocation = extract("v@:@") + //static let methodSignatureForSelector = extract("v@::") private static func extract(_ string: StaticString) -> UnsafePointer { return UnsafeRawPointer(string.utf8Start).assumingMemoryBound(to: CChar.self) @@ -24,6 +28,16 @@ class InterposeSubclass { /// Subclass that we create on the fly private(set) var dynamicClass: AnyClass + /// Hooks that have to be called dynamically. + var hookContainer: Interpose.DynamicHookContainer? { + get { objc_getAssociatedObject(object, &Interpose.AssociatedKeys.hookContainer) + as? Interpose.DynamicHookContainer } + set { + objc_setAssociatedObject(object, &Interpose.AssociatedKeys.hookContainer, + newValue, .OBJC_ASSOCIATION_RETAIN) + } + } + /// If the class has been altered (e.g. via NSKVONotifying_ KVO logic) /// then perceived and actual class don't match. /// @@ -75,7 +89,83 @@ class InterposeSubclass { return nil } + /// Overrides the invocation forwarding machinery to support dynamic invocation. + func prepareDynamicInvocation() throws { + guard InterposeSubclass.supportsSuperTrampolines else { throw InterposeError.unknownError("SuperBuilder is required for dynamic invocation")} + + replaceForwardInvocation() + } + + /// Looks for an instance method in this subclass, without looking up the hierarchy. + func exactClassImplementsSelector(_ selector: Selector) -> Bool { + exactClassImplementsSelector(dynamicClass, selector) + } + + /// Looks for an instance method in `klass`, without looking up the hierarchy. + func exactClassImplementsSelector(_ klass: AnyClass, _ selector: Selector) -> Bool { + var methodCount: CUnsignedInt = 0 + guard let methodsInAClass = class_copyMethodList(klass, &methodCount) else { return false } + defer { free(methodsInAClass) } + for index in 0 ..< Int(methodCount) { + let method = methodsInAClass[index] + if method_getName(method) == selector { + return true + } + } + return false + } + #if !os(Linux) + + class func aspectPrefixed(_ selector: Selector) -> Selector { + Selector("interpose_" + selector.description) + } + + /// Test if the class requires adding dynamic implementation preparation hooks. + private func requiresPrepareDynamicInvocation() -> Bool { + exactClassImplementsSelector( + dynamicClass, ObjCSelector.forwardInvocation) == false + } + + private func replaceForwardInvocation() { + guard requiresPrepareDynamicInvocation() else { return } + + // Add super trampoline + addSuperTrampoline(selector: ObjCSelector.forwardInvocation) + + // Replace with custom handler that calls our hooks + var origImp: IMP? + let forwardInvocation: @convention(block) (AnyObject, ObjCInvocation) -> Void = { bSelf, invocation in + + if let hookContainer = self.hookContainer { + hookContainer.before.executeAll(bSelf) + + // Call instead hooks or original + let instead = hookContainer.instead + if instead.isEmpty { + let selector = invocation.selector() + let prefixedSelector = InterposeSubclass.aspectPrefixed(selector) + invocation.setSelector(prefixedSelector) + invocation.invoke() + } else { + instead.executeAll(bSelf) + } + + hookContainer.after.executeAll(bSelf) + + } else { + // Call original forward + // - (void)forwardInvocation:(NSInvocation *)anInvocation + let originalInvocation = unsafeBitCast(origImp!, to: (@convention(c) (AnyObject, Selector, AnyObject) -> Void).self) + originalInvocation(bSelf, ObjCSelector.forwardInvocation, invocation) + + } + } + + let impl = imp_implementationWithBlock(forwardInvocation as Any) + origImp = class_replaceMethod(dynamicClass, ObjCSelector.forwardInvocation, impl, ObjCMethodEncoding.getClass) + } + private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) { // crashes on linux let getClass: @convention(block) (AnyObject) -> AnyClass = { _ in diff --git a/Sources/InterposeKit/ObjCInvocation.swift b/Sources/InterposeKit/ObjCInvocation.swift new file mode 100644 index 0000000..a11be1a --- /dev/null +++ b/Sources/InterposeKit/ObjCInvocation.swift @@ -0,0 +1,37 @@ +import Foundation + +public enum AspectStrategy { + case before /// Called before the original implementation. + case instead /// Called insted of the original implementation. + case after /// Called after the original implementation. +} + +/// `NSInvocation` is not directly accessible in Swift so we use a protocol. +@objc internal protocol ObjCInvocation { + @objc(setSelector:) + func setSelector(_ selector: Selector) + + @objc(selector) + func selector() -> Selector + + @objc(target) + var objcTarget: AnyObject { get } + + @objc(methodSignature) + var objcMethodSignature: AnyObject { get } + + @objc(getArgument:atIndex:) + func getArgument(_ argumentLocation: UnsafeMutableRawPointer, atIndex idx: Int) + + @objc(setArgument:atIndex:) + func setArgument(_ argumentLocation: UnsafeMutableRawPointer, atIndex idx: Int) + + @objc(invoke) + func invoke() + + @objc(invokeWithTarget:) + func invoke(target: AnyObject) + + @objc(invocationWithMethodSignature:) + static func invocation(methodSignature: AnyObject) -> AnyObject +} diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index 51bfd83..ca53f98 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -63,20 +63,6 @@ extension Interpose { return nil } - /// Looks for an instance method in the exact class, without looking up the hierarchy. - func exactClassImplementsSelector(_ klass: AnyClass, _ selector: Selector) -> Bool { - var methodCount: CUnsignedInt = 0 - guard let methodsInAClass = class_copyMethodList(klass, &methodCount) else { return false } - defer { free(methodsInAClass) } - for index in 0 ..< Int(methodCount) { - let method = methodsInAClass[index] - if method_getName(method) == selector { - return true - } - } - return false - } - var dynamicSubclass: AnyClass { interposeSubclass!.dynamicClass } @@ -94,7 +80,7 @@ extension Interpose { } // This function searches superclasses for implementations - let hasExistingMethod = exactClassImplementsSelector(dynamicSubclass, selector) + let hasExistingMethod = interposeSubclass!.exactClassImplementsSelector(dynamicSubclass, selector) let encoding = method_getTypeEncoding(method) if self.generatesSuperIMP { diff --git a/Sources/SuperBuilder/src/ITKSuperBuilder.m b/Sources/SuperBuilder/src/ITKSuperBuilder.m index 9101052..57b998a 100644 --- a/Sources/SuperBuilder/src/ITKSuperBuilder.m +++ b/Sources/SuperBuilder/src/ITKSuperBuilder.m @@ -112,6 +112,21 @@ static BOOL ITKMethodIsSuperTrampoline(Method method) { return methodIMP == (IMP)msgSendSuperTrampoline || methodIMP == (IMP)msgSendSuperStretTrampoline; } +bool prefix(const char *pre, const char *str) { + return strncmp(pre, str, strlen(pre)) == 0; +} + +let ITKInterposePrefix = "interpose_"; + +/// If prefix was found, forward the char pointer and get a new selector +SEL ITKReplaceCmd(__unsafe_unretained id obj, SEL _cmd); +SEL ITKReplaceCmd(__unsafe_unretained id obj, SEL _cmd) { + let len = strlen(ITKInterposePrefix); + let sel = sel_getName(_cmd); + let hasPrefix = strncmp(ITKInterposePrefix, sel, len) == 0; + return hasPrefix ? sel_getUid(sel + len) : _cmd; +} + struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL _cmd); struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL _cmd) { /** @@ -197,6 +212,7 @@ asm volatile ( // protect returned new value when we restore the pairs "mov x9, x0\n" + // pop {x0-x8, lr} "ldp x0, x1, [sp], #16\n" "ldp x2, x3, [sp], #16\n" @@ -249,14 +265,20 @@ asm volatile ( // // First parameter can be avoided, // but we need to keep the stack 16-byte algined. - //"movq %%rdi, -8(%%rbp) \n" // self po *(id *) - "movq %%rsi, -16(%%rbp) \n" // _cmd p (SEL)$rsi + "movq %%rdi, -8(%%rbp) \n" // self po *(id *) + //"movq %%rsi, -16(%%rbp) \n" // _cmd p (SEL)$rsi "movq %%rdx, -24(%%rbp) \n" // param 1 "movq %%rcx, -32(%%rbp) \n" // param 2 "movq %%r8, -40(%%rbp) \n" // param 3 "movq %%r9, -48(%%rbp) \n" // param 4 (rest goes on stack) + // replace _cmd + "callq _ITKReplaceCmd \n" + "movq %%rax, %%rsi \n" + "movq %%rsi, -16(%%rbp) \n" // store modified _cmd + // fetch filled struct objc_super, call with self + _cmd + "movq -8(%%rbp), %%rdi \n" "callq _ITKReturnThreadSuper \n" // first param is now struct objc_super "movq %%rax, %%rdi \n" diff --git a/Tests/InterposeKitTests/DynamicHookTests.swift b/Tests/InterposeKitTests/DynamicHookTests.swift new file mode 100644 index 0000000..06ae574 --- /dev/null +++ b/Tests/InterposeKitTests/DynamicHookTests.swift @@ -0,0 +1,33 @@ +import Foundation +import XCTest +@testable import InterposeKit + +final class DynamicInterposeTests: InterposeKitTestCase { + + func testDynamicSingleObject() throws { + let testObj = TestClass() + + // Test regular usage, calls block immediately + var executed = false + testObj.executeBlock { + executed = true + } + XCTAssertTrue(executed) + + // Add hook that is called before the block + var hookExecuted = false + _ = try testObj.hook(#selector(TestClass.executeBlock)) { bSelf in + print("Before Interposing Dynamic Hook for \(bSelf)") + hookExecuted = true + } + + // Ensure that hook is called before the block + executed = false + XCTAssertFalse(hookExecuted) + testObj.executeBlock { + // A before aspect is called before the block is executed + XCTAssertTrue(hookExecuted) + } + XCTAssertTrue(hookExecuted) + } +} diff --git a/Tests/InterposeKitTests/TestClass.swift b/Tests/InterposeKitTests/TestClass.swift index f754ef0..2c03de2 100644 --- a/Tests/InterposeKitTests/TestClass.swift +++ b/Tests/InterposeKitTests/TestClass.swift @@ -34,6 +34,10 @@ class TestClass: NSObject { return testClassHi } + @objc dynamic func executeBlock(_ block: () -> ()) { + block() + } + @objc dynamic func doNothing() { } @objc dynamic func doubleString(string: String) -> String { From 08a60bc4d8a039ff3c4f02c61551b33d43a40e60 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 20 Jun 2020 22:17:27 +0200 Subject: [PATCH 2/6] Add logic to remove dynamic hook --- Sources/InterposeKit/DynamicHook.swift | 27 ++++++++++++++++--- Sources/InterposeKit/InterposeSubclass.swift | 2 +- .../InterposeKitTests/DynamicHookTests.swift | 9 +++++-- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Sources/InterposeKit/DynamicHook.swift b/Sources/InterposeKit/DynamicHook.swift index d66a66e..07603ef 100644 --- a/Sources/InterposeKit/DynamicHook.swift +++ b/Sources/InterposeKit/DynamicHook.swift @@ -42,12 +42,13 @@ extension Interpose { try subclass.prepareDynamicInvocation() interposeSubclass = subclass - let hasExistingMethod = subclass.exactClassImplementsSelector(selector) - if !hasExistingMethod { + // If there is no existing implementation, add one. + if !subclass.exactClassImplements(selector: selector) { // Add super trampoline, then swizzle subclass.addSuperTrampoline(selector: selector) let superCallingMethod = class_getInstanceMethod(subclass.dynamicClass, selector)! + // add a prefixed copy of the method let aspectSelector = InterposeSubclass.aspectPrefixed(selector) let origImp = method_getImplementation(superCallingMethod) class_addMethod(subclass.dynamicClass, aspectSelector, origImp, encoding) @@ -70,9 +71,27 @@ extension Interpose { } override func resetImplementation() throws { - //let method = try validate(expectedState: .interposed) + let method = try validate(expectedState: .interposed) - // TODO + // Get the super-implementation via the prefixed method... + let aspectSelector = InterposeSubclass.aspectPrefixed(selector) + guard let dynamicClass = interposeSubclass?.dynamicClass, + let superIMP = class_getMethodImplementation(dynamicClass, aspectSelector) else { + throw InterposeError.unknownError("Unable to get subclass or met") + } + + // ... and replace the original + // The subclassed method can't be removed, but will be unused. + let encoding = method_getTypeEncoding(method) + let origIMP = class_replaceMethod(dynamicClass, selector, superIMP, encoding) + + // If the IMP does not match our expectations, throw! + // TODO: guard for dynamic + static hook mix! + guard origIMP == forwardIMP else { + throw InterposeError.unexpectedImplementation(dynamicClass, selector, origIMP) + } + + Interpose.log("Removed dynamic -[\(`class`).\(selector)]") } } diff --git a/Sources/InterposeKit/InterposeSubclass.swift b/Sources/InterposeKit/InterposeSubclass.swift index 441d5f5..718da71 100644 --- a/Sources/InterposeKit/InterposeSubclass.swift +++ b/Sources/InterposeKit/InterposeSubclass.swift @@ -97,7 +97,7 @@ class InterposeSubclass { } /// Looks for an instance method in this subclass, without looking up the hierarchy. - func exactClassImplementsSelector(_ selector: Selector) -> Bool { + func exactClassImplements(selector: Selector) -> Bool { exactClassImplementsSelector(dynamicClass, selector) } diff --git a/Tests/InterposeKitTests/DynamicHookTests.swift b/Tests/InterposeKitTests/DynamicHookTests.swift index 06ae574..bf2f11b 100644 --- a/Tests/InterposeKitTests/DynamicHookTests.swift +++ b/Tests/InterposeKitTests/DynamicHookTests.swift @@ -16,18 +16,23 @@ final class DynamicInterposeTests: InterposeKitTestCase { // Add hook that is called before the block var hookExecuted = false - _ = try testObj.hook(#selector(TestClass.executeBlock)) { bSelf in + let hook = try testObj.hook(#selector(TestClass.executeBlock)) { bSelf in print("Before Interposing Dynamic Hook for \(bSelf)") hookExecuted = true } // Ensure that hook is called before the block - executed = false XCTAssertFalse(hookExecuted) testObj.executeBlock { // A before aspect is called before the block is executed XCTAssertTrue(hookExecuted) } XCTAssertTrue(hookExecuted) + + try hook.revert() + + hookExecuted = false + testObj.executeBlock { } + XCTAssertFalse(hookExecuted) } } From 009edac3bd04f97b303491cdce3ea1645cb44aa6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 20 Jun 2020 22:30:13 +0200 Subject: [PATCH 3/6] Define super use at init time --- InterposeKit.xcodeproj/project.pbxproj | 2 +- Sources/InterposeKit/DynamicHook.swift | 18 +++++++++++++++++- Sources/InterposeKit/InterposeError.swift | 15 +++++++++++++++ Sources/InterposeKit/InterposeKit.swift | 2 +- Sources/InterposeKit/ObjCInvocation.swift | 6 ------ Sources/InterposeKit/ObjectHook.swift | 11 +++++++++-- 6 files changed, 43 insertions(+), 11 deletions(-) diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index d901102..6e0df47 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -191,11 +191,11 @@ 7810959D248D43DC008A943C /* ClassHook.swift */, 78EDB8FE248D0A9900D2F6C1 /* ObjectHook.swift */, 781EB557249BF0D1002545B4 /* DynamicHook.swift */, - 78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */, 7810959F248D50C1008A943C /* Watcher.swift */, 78E20D9724981B2A0021552C /* InterposeSubclass.swift */, 780FC9F9249822C900DA5A14 /* HookFinder.swift */, 781EB555249BEF54002545B4 /* ObjCInvocation.swift */, + 78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */, ); path = InterposeKit; sourceTree = ""; diff --git a/Sources/InterposeKit/DynamicHook.swift b/Sources/InterposeKit/DynamicHook.swift index 07603ef..04e042c 100644 --- a/Sources/InterposeKit/DynamicHook.swift +++ b/Sources/InterposeKit/DynamicHook.swift @@ -2,6 +2,12 @@ import Foundation extension Interpose { + public enum AspectStrategy { + case before /// Called before the original implementation. + case instead /// Called insted of the original implementation. + case after /// Called after the original implementation. + } + /// Hook that uses `NSInvocation `to not require specific signatures /// The call is converted into an invocation via `_objc_msgForward`. final public class DynamicHook: AnyHook { @@ -18,9 +24,19 @@ extension Interpose { /// Subclass that we create on the fly var interposeSubclass: InterposeSubclass? - public init(object: AnyObject, selector: Selector, + // Logic switch to use super builder + let generatesSuperIMP: Bool + + public init(object: AnyObject, + selector: Selector, strategy: AspectStrategy = .before, + generateSuper: Bool = true, implementation: @escaping (AnyObject) -> Void) throws { + if generateSuper && !InterposeSubclass.supportsSuperTrampolines { + throw InterposeError.superTrampolineNotAvailable + } + self.generatesSuperIMP = generateSuper + self.object = object self.strategy = strategy self.action = implementation diff --git a/Sources/InterposeKit/InterposeError.swift b/Sources/InterposeKit/InterposeError.swift index e19a372..4be86f8 100644 --- a/Sources/InterposeKit/InterposeError.swift +++ b/Sources/InterposeKit/InterposeError.swift @@ -34,6 +34,9 @@ public enum InterposeError: LocalizedError { /// Can't revert or apply if already done so. case invalidState(expectedState: AnyHook.State) + /// SuperBuilder is not in the runtime but required for this configuration. + case superTrampolineNotAvailable + /// Unable to remove hook. case resetUnsupported(_ reason: String) @@ -51,22 +54,34 @@ extension InterposeError: Equatable { switch self { case .methodNotFound(let klass, let selector): return "Method not found: -[\(klass) \(selector)]" + case .nonExistingImplementation(let klass, let selector): return "Implementation not found: -[\(klass) \(selector)]" + case .unexpectedImplementation(let klass, let selector, let IMP): return "Unexpected Implementation in -[\(klass) \(selector)]: \(String(describing: IMP))" + case .failedToAllocateClassPair(let klass, let subclassName): return "Failed to allocate class pair: \(klass), \(subclassName)" + case .unableToAddMethod(let klass, let selector): return "Unable to add method: -[\(klass) \(selector)]" + case .keyValueObservationDetected(let obj): return "Unable to hook object that uses Key Value Observing: \(obj)" + case .objectPosingAsDifferentClass(let obj, let actualClass): return "Unable to hook \(type(of: obj)) posing as \(NSStringFromClass(actualClass))/" + case .invalidState(let expectedState): return "Invalid State. Expected: \(expectedState)" + case .resetUnsupported(let reason): return "Reset Unsupported: \(reason)" + + case .superTrampolineNotAvailable: + return "SuperBuilder is required but not available at runtime." + case .unknownError(let reason): return reason } diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index 012f240..f78e742 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -18,7 +18,7 @@ extension NSObject { /// Hook an `@objc dynamic` instance method via selector on the current object or class.. @discardableResult public func hook ( _ selector: Selector, - strategy: AspectStrategy = .before, + strategy: Interpose.AspectStrategy = .before, _ implementation: @escaping (AnyObject) -> Void) throws -> AnyHook { return try Interpose.DynamicHook(object: self, selector: selector, strategy: strategy, implementation: implementation).apply() diff --git a/Sources/InterposeKit/ObjCInvocation.swift b/Sources/InterposeKit/ObjCInvocation.swift index a11be1a..814c696 100644 --- a/Sources/InterposeKit/ObjCInvocation.swift +++ b/Sources/InterposeKit/ObjCInvocation.swift @@ -1,11 +1,5 @@ import Foundation -public enum AspectStrategy { - case before /// Called before the original implementation. - case instead /// Called insted of the original implementation. - case after /// Called after the original implementation. -} - /// `NSInvocation` is not directly accessible in Swift so we use a protocol. @objc internal protocol ObjCInvocation { @objc(setSelector:) diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index ca53f98..f3f842f 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -13,11 +13,18 @@ extension Interpose { var interposeSubclass: InterposeSubclass? // Logic switch to use super builder - let generatesSuperIMP = InterposeSubclass.supportsSuperTrampolines + let generatesSuperIMP: Bool /// Initialize a new hook to interpose an instance method. - public init(object: AnyObject, selector: Selector, + public init(object: AnyObject, + selector: Selector, + generateSuper: Bool = true, implementation: (ObjectHook) -> HookSignature?) throws { + if generateSuper && !InterposeSubclass.supportsSuperTrampolines { + throw InterposeError.superTrampolineNotAvailable + } + self.generatesSuperIMP = generateSuper + self.object = object try super.init(class: type(of: object), selector: selector) let block = implementation(self) as AnyObject From 1ca89247c3fd2f8742fad41b5b3a51b57cc6ff97 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 20 Jun 2020 22:31:18 +0200 Subject: [PATCH 4/6] remove unnecesary return --- Sources/InterposeKit/InterposeKit.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index f78e742..e6867e8 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -20,7 +20,7 @@ extension NSObject { _ selector: Selector, strategy: Interpose.AspectStrategy = .before, _ implementation: @escaping (AnyObject) -> Void) throws -> AnyHook { - return try Interpose.DynamicHook(object: self, selector: selector, + try Interpose.DynamicHook(object: self, selector: selector, strategy: strategy, implementation: implementation).apply() } @@ -30,7 +30,7 @@ extension NSObject { methodSignature: MethodSignature.Type = MethodSignature.self, hookSignature: HookSignature.Type = HookSignature.self, _ implementation: (TypedHook) -> HookSignature?) throws -> AnyHook { - return try Interpose.ClassHook(class: self as AnyClass, + try Interpose.ClassHook(class: self as AnyClass, selector: selector, implementation: implementation).apply() } } From 1447aeca264548116aea5068be36944d6de39d41 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 21 Jun 2020 00:10:34 +0200 Subject: [PATCH 5/6] Add runtime helper --- InterposeKit.xcodeproj/project.pbxproj | 4 + Sources/InterposeKit/AnyHook.swift | 5 ++ Sources/InterposeKit/ClassHook.swift | 8 +- Sources/InterposeKit/DynamicHook.swift | 44 +++++----- Sources/InterposeKit/HookFinder.swift | 6 ++ Sources/InterposeKit/InterposeRuntime.swift | 87 ++++++++++++++++++++ Sources/InterposeKit/InterposeSubclass.swift | 51 ++++-------- Sources/InterposeKit/ObjectHook.swift | 48 ++++------- 8 files changed, 157 insertions(+), 96 deletions(-) create mode 100644 Sources/InterposeKit/InterposeRuntime.swift diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 6e0df47..31f96df 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 78A2F265249635B100F5AC5F /* KVOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F264249635B100F5AC5F /* KVOTests.swift */; }; 78A2F26724964AF200F5AC5F /* InterposeKitTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */; }; 78A2F26E2496B54B00F5AC5F /* InterposeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26D2496B54B00F5AC5F /* InterposeError.swift */; }; + 78AB64B5249EAED3002394CD /* InterposeRuntime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78AB64B4249EAED3002394CD /* InterposeRuntime.swift */; }; 78C39D7C2482CC7D00B46395 /* InterposeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; }; 78C39D8F2483164500B46395 /* InterposeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 78C39D8E2483164500B46395 /* InterposeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 78C39D912483165600B46395 /* InterposeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D902483165600B46395 /* InterposeKit.swift */; }; @@ -89,6 +90,7 @@ 78A2F264249635B100F5AC5F /* KVOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KVOTests.swift; path = Tests/InterposeKitTests/KVOTests.swift; sourceTree = SOURCE_ROOT; }; 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeKitTestCase.swift; path = Tests/InterposeKitTests/InterposeKitTestCase.swift; sourceTree = SOURCE_ROOT; }; 78A2F26D2496B54B00F5AC5F /* InterposeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeError.swift; path = Sources/InterposeKit/InterposeError.swift; sourceTree = SOURCE_ROOT; }; + 78AB64B4249EAED3002394CD /* InterposeRuntime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeRuntime.swift; path = Sources/InterposeKit/InterposeRuntime.swift; sourceTree = SOURCE_ROOT; }; 78C39D772482CC7D00B46395 /* InterposeKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InterposeKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = "Info-Tests.plist"; path = "InterposeKit.xcodeproj/Info-Tests.plist"; sourceTree = ""; }; 78C39D8E2483164500B46395 /* InterposeKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = InterposeKit.h; path = Sources/InterposeKit/InterposeKit.h; sourceTree = SOURCE_ROOT; }; @@ -196,6 +198,7 @@ 780FC9F9249822C900DA5A14 /* HookFinder.swift */, 781EB555249BEF54002545B4 /* ObjCInvocation.swift */, 78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */, + 78AB64B4249EAED3002394CD /* InterposeRuntime.swift */, ); path = InterposeKit; sourceTree = ""; @@ -417,6 +420,7 @@ 781EB556249BEF54002545B4 /* ObjCInvocation.swift in Sources */, 78E20D9824981B2A0021552C /* InterposeSubclass.swift in Sources */, 781EB558249BF0D1002545B4 /* DynamicHook.swift in Sources */, + 78AB64B5249EAED3002394CD /* InterposeRuntime.swift in Sources */, 78EDB8DD248BAA5600D2F6C1 /* AnyHook.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Sources/InterposeKit/AnyHook.swift b/Sources/InterposeKit/AnyHook.swift index 1c8d581..69d8fab 100644 --- a/Sources/InterposeKit/AnyHook.swift +++ b/Sources/InterposeKit/AnyHook.swift @@ -37,6 +37,11 @@ public class AnyHook { try validate() } + /// Helper to get class wrapper + var klass: InterposeClass { + InterposeClass(`class`) + } + func replaceImplementation() throws { preconditionFailure("Not implemented") } diff --git a/Sources/InterposeKit/ClassHook.swift b/Sources/InterposeKit/ClassHook.swift index fdbb748..1206a68 100644 --- a/Sources/InterposeKit/ClassHook.swift +++ b/Sources/InterposeKit/ClassHook.swift @@ -14,15 +14,17 @@ extension Interpose { override func replaceImplementation() throws { let method = try validate() - origIMP = class_replaceMethod(`class`, selector, replacementIMP, method_getTypeEncoding(method)) - guard origIMP != nil else { throw InterposeError.nonExistingImplementation(`class`, selector) } + + origIMP = try klass.replace(method: method, imp: replacementIMP) Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") } override func resetImplementation() throws { let method = try validate(expectedState: .interposed) precondition(origIMP != nil) - let previousIMP = class_replaceMethod(`class`, selector, origIMP!, method_getTypeEncoding(method)) + + + let previousIMP = try klass.replace(method: method, imp: origIMP!) guard previousIMP == replacementIMP else { throw InterposeError.unexpectedImplementation(`class`, selector, previousIMP) } diff --git a/Sources/InterposeKit/DynamicHook.swift b/Sources/InterposeKit/DynamicHook.swift index 04e042c..cf9387a 100644 --- a/Sources/InterposeKit/DynamicHook.swift +++ b/Sources/InterposeKit/DynamicHook.swift @@ -22,7 +22,7 @@ extension Interpose { public let action: (AnyObject) -> Void /// Subclass that we create on the fly - var interposeSubclass: InterposeSubclass? + var subclass: InterposeSubclass! // Logic switch to use super builder let generatesSuperIMP: Bool @@ -35,39 +35,44 @@ extension Interpose { if generateSuper && !InterposeSubclass.supportsSuperTrampolines { throw InterposeError.superTrampolineNotAvailable } - self.generatesSuperIMP = generateSuper self.object = object - self.strategy = strategy + self.strategy = strategy self.action = implementation + self.generatesSuperIMP = generateSuper try super.init(class: type(of: object), selector: selector) } private lazy var forwardIMP: IMP = { - let imp = dlsym(dlopen(nil, RTLD_LAZY), "_objc_msgForward") - return unsafeBitCast(imp, to: IMP.self) + resolve(symbol: "_objc_msgForward") + }() + + // stret is needed for x86-64 struct returns but not for ARM64 + private lazy var forwardStretIMP: IMP = { + resolve(symbol: "_objc_msgForward_stret") }() override func replaceImplementation() throws { let method = try validate() - let encoding = method_getTypeEncoding(method) // Check if there's an existing subclass we can reuse. // Create one at runtime if there is none. let subclass = try InterposeSubclass(object: object) try subclass.prepareDynamicInvocation() - interposeSubclass = subclass + self.subclass = subclass // If there is no existing implementation, add one. - if !subclass.exactClassImplements(selector: selector) { + if !subclass.implementsExact(selector: selector) { // Add super trampoline, then swizzle subclass.addSuperTrampoline(selector: selector) - let superCallingMethod = class_getInstanceMethod(subclass.dynamicClass, selector)! + let superCallingMethod = subclass.instanceMethod(selector)! // add a prefixed copy of the method let aspectSelector = InterposeSubclass.aspectPrefixed(selector) - let origImp = method_getImplementation(superCallingMethod) - class_addMethod(subclass.dynamicClass, aspectSelector, origImp, encoding) + let origImp = superCallingMethod.implementation + + try subclass.add(selector: aspectSelector, imp: origImp, encoding: method.typeEncoding) + Interpose.log("Generated -[\(`class`).\(aspectSelector)]: \(origImp)") } @@ -78,11 +83,7 @@ extension Interpose { newContainer.hooks = hooks subclass.hookContainer = newContainer - let forwardIMP = self.forwardIMP - guard class_replaceMethod(subclass.dynamicClass, selector, forwardIMP, encoding) != nil else { - throw InterposeError.unableToAddMethod(subclass.dynamicClass, selector) - } - + try subclass.replace(method: method, imp: self.forwardIMP) Interpose.log("Added dynamic -[\(`class`).\(selector)]") } @@ -91,20 +92,17 @@ extension Interpose { // Get the super-implementation via the prefixed method... let aspectSelector = InterposeSubclass.aspectPrefixed(selector) - guard let dynamicClass = interposeSubclass?.dynamicClass, - let superIMP = class_getMethodImplementation(dynamicClass, aspectSelector) else { - throw InterposeError.unknownError("Unable to get subclass or met") - } + + let superIMP = try subclass.methodImplementation(aspectSelector) // ... and replace the original // The subclassed method can't be removed, but will be unused. - let encoding = method_getTypeEncoding(method) - let origIMP = class_replaceMethod(dynamicClass, selector, superIMP, encoding) + let origIMP = try subclass.replace(method: method, imp: superIMP) // If the IMP does not match our expectations, throw! // TODO: guard for dynamic + static hook mix! guard origIMP == forwardIMP else { - throw InterposeError.unexpectedImplementation(dynamicClass, selector, origIMP) + throw InterposeError.unexpectedImplementation(subclass.class, selector, origIMP) } Interpose.log("Removed dynamic -[\(`class`).\(selector)]") diff --git a/Sources/InterposeKit/HookFinder.swift b/Sources/InterposeKit/HookFinder.swift index 30ce805..7e0d0c0 100644 --- a/Sources/InterposeKit/HookFinder.swift +++ b/Sources/InterposeKit/HookFinder.swift @@ -18,6 +18,12 @@ extension Interpose { } } + /// Helper to resolve an implementation + static func resolve(symbol: String) -> IMP { + let imp = dlsym(dlopen(nil, RTLD_LAZY), symbol) + return unsafeBitCast(imp, to: IMP.self) + } + static func storeHook(hook: HookType, to block: AnyObject) { // Weakly store reference to hook inside the block of the IMP. objc_setAssociatedObject(block, &AssociatedKeys.hookForBlock, diff --git a/Sources/InterposeKit/InterposeRuntime.swift b/Sources/InterposeKit/InterposeRuntime.swift new file mode 100644 index 0000000..6945b03 --- /dev/null +++ b/Sources/InterposeKit/InterposeRuntime.swift @@ -0,0 +1,87 @@ +import Foundation + +extension Method { + var implementation: IMP { + method_getImplementation(self) + } + + var typeEncoding: UnsafePointer? { + method_getTypeEncoding(self) + } + + var name: Selector { + method_getName(self) + } +} + +/// Looks for an instance method in `klass`, without looking up the hierarchy. +func implementsExactClass(klass: AnyClass, selector: Selector) -> Bool { + var methodCount: CUnsignedInt = 0 + guard let methodsInAClass = class_copyMethodList(klass, &methodCount) else { return false } + defer { free(methodsInAClass) } + for index in 0 ..< Int(methodCount) { + let method = methodsInAClass[index] + if method_getName(method) == selector { + return true + } + } + return false +} + +struct InterposeClass: ModifyableClass { + let `class`: AnyClass + + init(_ class: AnyClass) { + self.`class` = `class` + } +} + +protocol ModifyableClass { + var `class`: AnyClass { get } + func add(selector: Selector, imp: IMP, encoding: UnsafePointer?) throws + @discardableResult func replace(method: Method, imp: IMP) throws -> IMP + @discardableResult func replace(selector: Selector, imp: IMP, encoding: UnsafePointer?) throws -> IMP + func implementsExact(selector: Selector) -> Bool + func instanceMethod(_ selector: Selector) -> Method? + func methodImplementation(_ selector: Selector) throws -> IMP +} + +extension ModifyableClass { + var superclass: AnyClass? { + class_getSuperclass(`class`) + } + + func add(selector: Selector, imp: IMP, encoding: UnsafePointer?) throws { + if !class_addMethod(`class`, selector, imp, encoding) { + Interpose.log("Unable to add: -[\(`class`).\(selector)] IMP: \(imp)") + throw InterposeError.unableToAddMethod(`class`, selector) + } + } + + @discardableResult func replace(method: Method, imp: IMP) throws -> IMP { + try replace(selector: method.name, imp: imp, encoding: method.typeEncoding) + } + + @discardableResult func replace(selector: Selector, imp: IMP, encoding: UnsafePointer?) throws -> IMP { + guard let imp = class_replaceMethod(`class`, selector, imp, encoding) else { + throw InterposeError.unableToAddMethod(`class`, selector) + } + return imp + } + + /// Looks for an instance method in this subclass, without looking up the hierarchy. + func implementsExact(selector: Selector) -> Bool { + implementsExactClass(klass: `class`, selector: selector) + } + + func instanceMethod(_ selector: Selector) -> Method? { + class_getInstanceMethod(`class`, selector) + } + + func methodImplementation(_ selector: Selector) throws -> IMP { + guard let imp = class_getMethodImplementation(`class`, selector) else { + throw InterposeError.unknownError("Unable to get method implementation") + } + return imp + } +} diff --git a/Sources/InterposeKit/InterposeSubclass.swift b/Sources/InterposeKit/InterposeSubclass.swift index 718da71..5ffcd7f 100644 --- a/Sources/InterposeKit/InterposeSubclass.swift +++ b/Sources/InterposeKit/InterposeSubclass.swift @@ -1,6 +1,6 @@ import Foundation -class InterposeSubclass { +class InterposeSubclass: ModifyableClass { private enum Constants { static let subclassSuffix = "InterposeKit_" @@ -26,15 +26,15 @@ class InterposeSubclass { let object: AnyObject /// Subclass that we create on the fly - private(set) var dynamicClass: AnyClass + private(set) var `class`: AnyClass /// Hooks that have to be called dynamically. var hookContainer: Interpose.DynamicHookContainer? { get { objc_getAssociatedObject(object, &Interpose.AssociatedKeys.hookContainer) - as? Interpose.DynamicHookContainer } + as? Interpose.DynamicHookContainer } set { objc_setAssociatedObject(object, &Interpose.AssociatedKeys.hookContainer, - newValue, .OBJC_ASSOCIATION_RETAIN) + newValue, .OBJC_ASSOCIATION_RETAIN) } } @@ -45,8 +45,8 @@ class InterposeSubclass { /// If we make a dynamic subclass over KVO, invalidating the token crashes in cache_getImp. init(object: AnyObject) throws { self.object = object - dynamicClass = type(of: object) // satisfy set to something - dynamicClass = try getExistingSubclass() ?? createSubclass() + `class` = type(of: object) // satisfy set to something + `class` = try getExistingSubclass() ?? createSubclass() } private func createSubclass() throws -> AnyClass { @@ -93,26 +93,7 @@ class InterposeSubclass { func prepareDynamicInvocation() throws { guard InterposeSubclass.supportsSuperTrampolines else { throw InterposeError.unknownError("SuperBuilder is required for dynamic invocation")} - replaceForwardInvocation() - } - - /// Looks for an instance method in this subclass, without looking up the hierarchy. - func exactClassImplements(selector: Selector) -> Bool { - exactClassImplementsSelector(dynamicClass, selector) - } - - /// Looks for an instance method in `klass`, without looking up the hierarchy. - func exactClassImplementsSelector(_ klass: AnyClass, _ selector: Selector) -> Bool { - var methodCount: CUnsignedInt = 0 - guard let methodsInAClass = class_copyMethodList(klass, &methodCount) else { return false } - defer { free(methodsInAClass) } - for index in 0 ..< Int(methodCount) { - let method = methodsInAClass[index] - if method_getName(method) == selector { - return true - } - } - return false + try replaceForwardInvocation() } #if !os(Linux) @@ -123,11 +104,10 @@ class InterposeSubclass { /// Test if the class requires adding dynamic implementation preparation hooks. private func requiresPrepareDynamicInvocation() -> Bool { - exactClassImplementsSelector( - dynamicClass, ObjCSelector.forwardInvocation) == false + implementsExact(selector: ObjCSelector.forwardInvocation) == false } - private func replaceForwardInvocation() { + private func replaceForwardInvocation() throws { guard requiresPrepareDynamicInvocation() else { return } // Add super trampoline @@ -163,7 +143,7 @@ class InterposeSubclass { } let impl = imp_implementationWithBlock(forwardInvocation as Any) - origImp = class_replaceMethod(dynamicClass, ObjCSelector.forwardInvocation, impl, ObjCMethodEncoding.getClass) + origImp = try replace(selector: ObjCSelector.forwardInvocation, imp: impl, encoding: ObjCMethodEncoding.getClass) } private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) { @@ -181,18 +161,17 @@ class InterposeSubclass { } private lazy var addSuperImpl: @convention(c) (AnyClass, Selector, NSErrorPointer) -> Bool = { - let handle = dlopen(nil, RTLD_LAZY) - let imp = dlsym(handle, "IKTAddSuperImplementationToClass") + let imp = Interpose.resolve(symbol: "IKTAddSuperImplementationToClass") return unsafeBitCast(imp, to: (@convention(c) (AnyClass, Selector, NSErrorPointer) -> Bool).self) }() func addSuperTrampoline(selector: Selector) { var error: NSError? - if addSuperImpl(dynamicClass, selector, &error) == false { - Interpose.log("Failed to add super implementation to -[\(dynamicClass).\(selector)]: \(error!)") + if addSuperImpl(`class`, selector, &error) == false { + Interpose.log("Failed to add super implementation to -[\(`class`).\(selector)]: \(error!)") } else { - let imp = class_getMethodImplementation(dynamicClass, selector)! - Interpose.log("Added super for -[\(dynamicClass).\(selector)]: \(imp)") + let imp = class_getMethodImplementation(`class`, selector)! + Interpose.log("Added super for -[\(`class`).\(selector)]: \(imp)") } } #else diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index f3f842f..c77bcfb 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -10,7 +10,8 @@ extension Interpose { public let object: AnyObject /// Subclass that we create on the fly - var interposeSubclass: InterposeSubclass? + /// Generated when replace is called, checked via state. + var subclass: InterposeSubclass! // Logic switch to use super builder let generatesSuperIMP: Bool @@ -62,7 +63,7 @@ extension Interpose { repeat { if let currentClass = currentClass, let method = class_getInstanceMethod(currentClass, self.selector) { - let origIMP = method_getImplementation(method) + let origIMP = method.implementation return unsafeBitCast(origIMP, to: MethodSignature.self) } currentClass = class_getSuperclass(currentClass) @@ -70,16 +71,12 @@ extension Interpose { return nil } - var dynamicSubclass: AnyClass { - interposeSubclass!.dynamicClass - } - override func replaceImplementation() throws { let method = try validate() // Check if there's an existing subclass we can reuse. // Create one at runtime if there is none. - interposeSubclass = try InterposeSubclass(object: object) + subclass = try InterposeSubclass(object: object) // The implementation of the call that is hooked must exist. guard lookupOrigIMP != nil else { @@ -87,40 +84,26 @@ extension Interpose { } // This function searches superclasses for implementations - let hasExistingMethod = interposeSubclass!.exactClassImplementsSelector(dynamicSubclass, selector) - let encoding = method_getTypeEncoding(method) + let hasExistingMethod = subclass!.implementsExact(selector: selector) if self.generatesSuperIMP { // If the subclass is empty, we create a super trampoline first. // If a hook already exists, we must skip this. if !hasExistingMethod { - interposeSubclass!.addSuperTrampoline(selector: selector) + subclass!.addSuperTrampoline(selector: selector) } // Replace IMP (by now we guarantee that it exists) - origIMP = class_replaceMethod(dynamicSubclass, selector, replacementIMP, encoding) - guard origIMP != nil else { - throw InterposeError.nonExistingImplementation(dynamicSubclass, selector) - } + origIMP = try subclass.replace(method: method, imp: replacementIMP) Interpose.log("Added -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") } else { // Could potentially be unified in the code paths if hasExistingMethod { - origIMP = class_replaceMethod(dynamicSubclass, selector, replacementIMP, encoding) - if origIMP != nil { - Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!) via replacement") - } else { - Interpose.log("Unable to replace: -[\(`class`).\(selector)] IMP: \(replacementIMP!)") - throw InterposeError.unableToAddMethod(`class`, selector) - } + origIMP = try subclass.replace(method: method, imp: replacementIMP) + Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!) via replacement") } else { - let didAddMethod = class_addMethod(dynamicSubclass, selector, replacementIMP, encoding) - if didAddMethod { - Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!)") - } else { - Interpose.log("Unable to add: -[\(`class`).\(selector)] IMP: \(replacementIMP!)") - throw InterposeError.unableToAddMethod(`class`, selector) - } + try subclass.add(selector: selector, imp: replacementIMP, encoding: method.typeEncoding) + Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!)") } } } @@ -140,16 +123,13 @@ extension Interpose { throw InterposeError.resetUnsupported("No Original IMP found. SuperBuilder missing?") } - guard let currentIMP = class_getMethodImplementation(dynamicSubclass, selector) else { - throw InterposeError.unknownError("No Implementation found") - } + let currentIMP = try subclass.methodImplementation(selector) // We are the topmost hook, replace method. if currentIMP == replacementIMP { - let previousIMP = class_replaceMethod( - dynamicSubclass, selector, origIMP!, method_getTypeEncoding(method)) + let previousIMP = try subclass.replace(method: method, imp: origIMP!) guard previousIMP == replacementIMP else { - throw InterposeError.unexpectedImplementation(dynamicSubclass, selector, previousIMP) + throw InterposeError.unexpectedImplementation(subclass.class, selector, previousIMP) } Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") } else { From 89f32cc52bfb948a2900a3a3ba49d240368a4b27 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 21 Jun 2020 00:13:46 +0200 Subject: [PATCH 6/6] use make, not generate --- Sources/InterposeKit/DynamicHook.swift | 10 +++++----- Sources/InterposeKit/InterposeKit.swift | 4 ++-- Sources/InterposeKit/InterposeSubclass.swift | 2 +- Sources/InterposeKit/ObjectHook.swift | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Sources/InterposeKit/DynamicHook.swift b/Sources/InterposeKit/DynamicHook.swift index cf9387a..e15bca7 100644 --- a/Sources/InterposeKit/DynamicHook.swift +++ b/Sources/InterposeKit/DynamicHook.swift @@ -25,21 +25,21 @@ extension Interpose { var subclass: InterposeSubclass! // Logic switch to use super builder - let generatesSuperIMP: Bool + let makesSuperIMP: Bool public init(object: AnyObject, selector: Selector, strategy: AspectStrategy = .before, - generateSuper: Bool = true, + makeSuper: Bool = true, implementation: @escaping (AnyObject) -> Void) throws { - if generateSuper && !InterposeSubclass.supportsSuperTrampolines { + if makeSuper && !InterposeSubclass.supportsSuperTrampolines { throw InterposeError.superTrampolineNotAvailable } self.object = object self.strategy = strategy self.action = implementation - self.generatesSuperIMP = generateSuper + self.makesSuperIMP = makeSuper try super.init(class: type(of: object), selector: selector) } @@ -73,7 +73,7 @@ extension Interpose { try subclass.add(selector: aspectSelector, imp: origImp, encoding: method.typeEncoding) - Interpose.log("Generated -[\(`class`).\(aspectSelector)]: \(origImp)") + Interpose.log("maked -[\(`class`).\(aspectSelector)]: \(origImp)") } // append hook as copy diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index e6867e8..7b8ef09 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -60,7 +60,7 @@ final public class Interpose { } // This is based on observation, there is no documented way - private func isKVORuntimeGeneratedClass(_ klass: AnyClass) -> Bool { + private func isKVORuntimemakedClass(_ klass: AnyClass) -> Bool { NSStringFromClass(klass).hasPrefix("NSKVO") } @@ -82,7 +82,7 @@ final public class Interpose { self.class = type(of: object) if let actualClass = checkObjectPosingAsDifferentClass(object) { - if isKVORuntimeGeneratedClass(actualClass) { + if isKVORuntimemakedClass(actualClass) { throw InterposeError.keyValueObservationDetected(object) } else { throw InterposeError.objectPosingAsDifferentClass(object, actualClass: actualClass) diff --git a/Sources/InterposeKit/InterposeSubclass.swift b/Sources/InterposeKit/InterposeSubclass.swift index 5ffcd7f..8ee52a9 100644 --- a/Sources/InterposeKit/InterposeSubclass.swift +++ b/Sources/InterposeKit/InterposeSubclass.swift @@ -76,7 +76,7 @@ class InterposeSubclass: ModifyableClass { object_setClass(object, nnSubclass) let oldName = NSStringFromClass(class_getSuperclass(object_getClass(object)!)!) - Interpose.log("Generated \(NSStringFromClass(nnSubclass)) for object (was: \(oldName))") + Interpose.log("maked \(NSStringFromClass(nnSubclass)) for object (was: \(oldName))") return nnSubclass } diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index c77bcfb..d6a49c2 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -10,21 +10,21 @@ extension Interpose { public let object: AnyObject /// Subclass that we create on the fly - /// Generated when replace is called, checked via state. + /// maked when replace is called, checked via state. var subclass: InterposeSubclass! // Logic switch to use super builder - let generatesSuperIMP: Bool + let makesSuperIMP: Bool /// Initialize a new hook to interpose an instance method. public init(object: AnyObject, selector: Selector, - generateSuper: Bool = true, + makeSuper: Bool = true, implementation: (ObjectHook) -> HookSignature?) throws { - if generateSuper && !InterposeSubclass.supportsSuperTrampolines { + if makeSuper && !InterposeSubclass.supportsSuperTrampolines { throw InterposeError.superTrampolineNotAvailable } - self.generatesSuperIMP = generateSuper + self.makesSuperIMP = makeSuper self.object = object try super.init(class: type(of: object), selector: selector) @@ -86,7 +86,7 @@ extension Interpose { // This function searches superclasses for implementations let hasExistingMethod = subclass!.implementsExact(selector: selector) - if self.generatesSuperIMP { + if self.makesSuperIMP { // If the subclass is empty, we create a super trampoline first. // If a hook already exists, we must skip this. if !hasExistingMethod {