diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d5d4ee..b46cf80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * Preserve object-hook super trampolines in optimized Release builds, fixing https://github.com/steipete/InterposeKit/issues/29. Thanks to [@Thomvis](https://github.com/Thomvis). * Preserve floating-point arguments when object hooks invoke original methods on arm64. Thanks to [@ishutinvv](https://github.com/ishutinvv). * Run class-availability hooks after Objective-C loads a new image, fixing https://github.com/steipete/InterposeKit/issues/26. +* Reject Core Foundation-backed object hooks before dynamic subclassing, preventing crashes such as https://github.com/steipete/InterposeKit/issues/23. ## 0.01 diff --git a/README.md b/README.md index f57228f..a608a58 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ Hi there 👋 and Interpose InterposeKit can hook classes and object. Class hooking is similar to swizzling, but object-based hooking offers a variety of new ways to set hooks. This is achieved via creating a dynamic subclass at runtime. -Caveat: Hooking will fail with an error if the object uses KVO. The KVO machinery is fragile and it's to easy to cause a crash. Using KVO after a hook was created is supported and will not cause issues. +Caveat: Hooking will fail with an error if the object uses KVO or is backed by Core Foundation, such as `NSURL`. These objects rely on runtime behavior that is incompatible with InterposeKit's dynamic subclass. Using KVO after a hook was created is supported and will not cause issues. ## Various ways to define the signature diff --git a/Sources/InterposeKit/InterposeError.swift b/Sources/InterposeKit/InterposeError.swift index e19a372..8736e1e 100644 --- a/Sources/InterposeKit/InterposeError.swift +++ b/Sources/InterposeKit/InterposeError.swift @@ -23,6 +23,9 @@ public enum InterposeError: LocalizedError { /// Adding a hook eventually crashes the KVO management code so we reject hooking altogether in this case. case keyValueObservationDetected(AnyObject) + /// Core Foundation-backed objects do not support isa-swizzling from Swift. + case coreFoundationObjectDetected(AnyObject) + /// Object is lying about it's actual class metadata. /// This usually happens when other swizzling libraries (like Aspects) also interfere with a class. /// While this might just work, it's not worth risking a crash, so similar to KVO this case is rejected. @@ -61,6 +64,8 @@ extension InterposeError: Equatable { return "Unable to add method: -[\(klass) \(selector)]" case .keyValueObservationDetected(let obj): return "Unable to hook object that uses Key Value Observing: \(obj)" + case .coreFoundationObjectDetected(let obj): + return "Unable to hook Core Foundation-backed object: \(obj)" case .objectPosingAsDifferentClass(let obj, let actualClass): return "Unable to hook \(type(of: obj)) posing as \(NSStringFromClass(actualClass))/" case .invalidState(let expectedState): diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index 90d9270..b46ac1f 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -1,3 +1,6 @@ +#if !os(Linux) +import CoreFoundation +#endif import Foundation extension NSObject { @@ -41,7 +44,7 @@ final public class Interpose { // Checks if a object is posing as a different class // via implementing 'class' and returning something else. - private func checkObjectPosingAsDifferentClass(_ object: AnyObject) -> AnyClass? { + private static func checkObjectPosingAsDifferentClass(_ object: AnyObject) -> AnyClass? { let perceivedClass: AnyClass = type(of: object) let actualClass: AnyClass = object_getClass(object)! if actualClass != perceivedClass { @@ -51,10 +54,35 @@ final public class Interpose { } // This is based on observation, there is no documented way - private func isKVORuntimeGeneratedClass(_ klass: AnyClass) -> Bool { + private static func isKVORuntimeGeneratedClass(_ klass: AnyClass) -> Bool { String(cString: class_getName(klass)).contains("NSKVONotifying_") } + #if !os(Linux) + private static let objectiveCObjectTypeID = CFGetTypeID(NSObject()) + #endif + + private static func isCoreFoundationBackedObject(_ object: AnyObject) -> Bool { + #if os(Linux) + return false + #else + return CFGetTypeID(object as CFTypeRef) != objectiveCObjectTypeID + #endif + } + + static func validateObjectForHooking(_ object: AnyObject) throws { + if let actualClass = checkObjectPosingAsDifferentClass(object) { + if isKVORuntimeGeneratedClass(actualClass) { + throw InterposeError.keyValueObservationDetected(object) + } else if !InterposeSubclass.isInterposeSubclass(actualClass) { + throw InterposeError.objectPosingAsDifferentClass(object, actualClass: actualClass) + } + } + if isCoreFoundationBackedObject(object) { + throw InterposeError.coreFoundationObjectDetected(object) + } + } + /// Initializes an instance of Interpose for a specific class. /// If `builder` is present, `apply()` is automatically called. public init(_ `class`: AnyClass, builder: ((Interpose) throws -> Void)? = nil) throws { @@ -72,13 +100,7 @@ final public class Interpose { self.object = object self.class = type(of: object) - if let actualClass = checkObjectPosingAsDifferentClass(object) { - if isKVORuntimeGeneratedClass(actualClass) { - throw InterposeError.keyValueObservationDetected(object) - } else { - throw InterposeError.objectPosingAsDifferentClass(object, actualClass: actualClass) - } - } + try Self.validateObjectForHooking(object) // Only apply if a builder is present if let builder = builder { diff --git a/Sources/InterposeKit/InterposeSubclass.swift b/Sources/InterposeKit/InterposeSubclass.swift index 7752ce1..11c028b 100644 --- a/Sources/InterposeKit/InterposeSubclass.swift +++ b/Sources/InterposeKit/InterposeSubclass.swift @@ -69,12 +69,16 @@ class InterposeSubclass { /// We need to reuse a dynamic subclass if the object already has one. private func getExistingSubclass() -> AnyClass? { let actualClass: AnyClass = object_getClass(object)! - if NSStringFromClass(actualClass).hasPrefix(Constants.subclassSuffix) { + if Self.isInterposeSubclass(actualClass) { return actualClass } return nil } + static func isInterposeSubclass(_ klass: AnyClass) -> Bool { + NSStringFromClass(klass).hasPrefix(Constants.subclassSuffix) + } + #if !os(Linux) private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) { // crashes on linux diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index 51bfd83..09ba632 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -18,6 +18,7 @@ extension Interpose { /// Initialize a new hook to interpose an instance method. public init(object: AnyObject, selector: Selector, implementation: (ObjectHook) -> HookSignature?) throws { + try Interpose.validateObjectForHooking(object) self.object = object try super.init(class: type(of: object), selector: selector) let block = implementation(self) as AnyObject diff --git a/Tests/InterposeKitTests/ObjectInterposeTests.swift b/Tests/InterposeKitTests/ObjectInterposeTests.swift index 596f5b2..b1d810d 100644 --- a/Tests/InterposeKitTests/ObjectInterposeTests.swift +++ b/Tests/InterposeKitTests/ObjectInterposeTests.swift @@ -4,6 +4,26 @@ import XCTest final class ObjectInterposeTests: InterposeKitTestCase { + func testRejectsCoreFoundationBackedObject() throws { + let url = try XCTUnwrap(NSURL(string: "https://www.google.com")) + let expectedError = InterposeError.coreFoundationObjectDetected(url) + + XCTAssertThrowsError(try Interpose(url)) { error in + XCTAssertEqual(error as? InterposeError, expectedError) + } + XCTAssertThrowsError(try url.hook( + #selector(getter: NSURL.host), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature: (@convention(block) (AnyObject) -> String).self + ) { _ in + { _ in "www.facebook.com" } + }) { error in + XCTAssertEqual(error as? InterposeError, expectedError) + } + + XCTAssertEqual(url.host, "www.google.com") + } + func testInterposeSingleObject() throws { let testObj = TestClass() let testObj2 = TestClass()