Skip to content
This repository was archived by the owner on Jun 11, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions Sources/InterposeKit/InterposeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down
40 changes: 31 additions & 9 deletions Sources/InterposeKit/InterposeKit.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#if !os(Linux)
import CoreFoundation
#endif
import Foundation

extension NSObject {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
6 changes: 5 additions & 1 deletion Sources/InterposeKit/InterposeSubclass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Sources/InterposeKit/ObjectHook.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ extension Interpose {
/// Initialize a new hook to interpose an instance method.
public init(object: AnyObject, selector: Selector,
implementation: (ObjectHook<MethodSignature, HookSignature>) -> HookSignature?) throws {
try Interpose.validateObjectForHooking(object)
self.object = object
try super.init(class: type(of: object), selector: selector)
let block = implementation(self) as AnyObject
Expand Down
20 changes: 20 additions & 0 deletions Tests/InterposeKitTests/ObjectInterposeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@

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" }

Check warning on line 19 in Tests/InterposeKitTests/ObjectInterposeTests.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Opening Brace Spacing Violation: Opening braces should be preceded by a single space and on the same line as the declaration. (opening_brace)
}) { error in
XCTAssertEqual(error as? InterposeError, expectedError)
}

XCTAssertEqual(url.host, "www.google.com")
}

func testInterposeSingleObject() throws {
let testObj = TestClass()
let testObj2 = TestClass()
Expand Down
Loading