Skip to content
This repository was archived by the owner on Jun 11, 2026. It is now read-only.

Commit a7e1b57

Browse files
authored
Merge pull request #46 from steipete/fix/nsurl-object-hook
Reject unsafe Core Foundation object hooks
2 parents 20945a4 + 7413a06 commit a7e1b57

7 files changed

Lines changed: 64 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* Preserve object-hook super trampolines in optimized Release builds, fixing https://github.com/steipete/InterposeKit/issues/29. Thanks to [@Thomvis](https://github.com/Thomvis).
1515
* Preserve floating-point arguments when object hooks invoke original methods on arm64. Thanks to [@ishutinvv](https://github.com/ishutinvv).
1616
* Run class-availability hooks after Objective-C loads a new image, fixing https://github.com/steipete/InterposeKit/issues/26.
17+
* Reject Core Foundation-backed object hooks before dynamic subclassing, preventing crashes such as https://github.com/steipete/InterposeKit/issues/23.
1718

1819
## 0.01
1920

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Hi there 👋 and Interpose
8585

8686
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.
8787

88-
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.
88+
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.
8989

9090
## Various ways to define the signature
9191

Sources/InterposeKit/InterposeError.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public enum InterposeError: LocalizedError {
2323
/// Adding a hook eventually crashes the KVO management code so we reject hooking altogether in this case.
2424
case keyValueObservationDetected(AnyObject)
2525

26+
/// Core Foundation-backed objects do not support isa-swizzling from Swift.
27+
case coreFoundationObjectDetected(AnyObject)
28+
2629
/// Object is lying about it's actual class metadata.
2730
/// This usually happens when other swizzling libraries (like Aspects) also interfere with a class.
2831
/// 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 {
6164
return "Unable to add method: -[\(klass) \(selector)]"
6265
case .keyValueObservationDetected(let obj):
6366
return "Unable to hook object that uses Key Value Observing: \(obj)"
67+
case .coreFoundationObjectDetected(let obj):
68+
return "Unable to hook Core Foundation-backed object: \(obj)"
6469
case .objectPosingAsDifferentClass(let obj, let actualClass):
6570
return "Unable to hook \(type(of: obj)) posing as \(NSStringFromClass(actualClass))/"
6671
case .invalidState(let expectedState):

Sources/InterposeKit/InterposeKit.swift

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
#if !os(Linux)
2+
import CoreFoundation
3+
#endif
14
import Foundation
25

36
extension NSObject {
@@ -41,7 +44,7 @@ final public class Interpose {
4144

4245
// Checks if a object is posing as a different class
4346
// via implementing 'class' and returning something else.
44-
private func checkObjectPosingAsDifferentClass(_ object: AnyObject) -> AnyClass? {
47+
private static func checkObjectPosingAsDifferentClass(_ object: AnyObject) -> AnyClass? {
4548
let perceivedClass: AnyClass = type(of: object)
4649
let actualClass: AnyClass = object_getClass(object)!
4750
if actualClass != perceivedClass {
@@ -51,10 +54,35 @@ final public class Interpose {
5154
}
5255

5356
// This is based on observation, there is no documented way
54-
private func isKVORuntimeGeneratedClass(_ klass: AnyClass) -> Bool {
57+
private static func isKVORuntimeGeneratedClass(_ klass: AnyClass) -> Bool {
5558
String(cString: class_getName(klass)).contains("NSKVONotifying_")
5659
}
5760

61+
#if !os(Linux)
62+
private static let objectiveCObjectTypeID = CFGetTypeID(NSObject())
63+
#endif
64+
65+
private static func isCoreFoundationBackedObject(_ object: AnyObject) -> Bool {
66+
#if os(Linux)
67+
return false
68+
#else
69+
return CFGetTypeID(object as CFTypeRef) != objectiveCObjectTypeID
70+
#endif
71+
}
72+
73+
static func validateObjectForHooking(_ object: AnyObject) throws {
74+
if let actualClass = checkObjectPosingAsDifferentClass(object) {
75+
if isKVORuntimeGeneratedClass(actualClass) {
76+
throw InterposeError.keyValueObservationDetected(object)
77+
} else if !InterposeSubclass.isInterposeSubclass(actualClass) {
78+
throw InterposeError.objectPosingAsDifferentClass(object, actualClass: actualClass)
79+
}
80+
}
81+
if isCoreFoundationBackedObject(object) {
82+
throw InterposeError.coreFoundationObjectDetected(object)
83+
}
84+
}
85+
5886
/// Initializes an instance of Interpose for a specific class.
5987
/// If `builder` is present, `apply()` is automatically called.
6088
public init(_ `class`: AnyClass, builder: ((Interpose) throws -> Void)? = nil) throws {
@@ -72,13 +100,7 @@ final public class Interpose {
72100
self.object = object
73101
self.class = type(of: object)
74102

75-
if let actualClass = checkObjectPosingAsDifferentClass(object) {
76-
if isKVORuntimeGeneratedClass(actualClass) {
77-
throw InterposeError.keyValueObservationDetected(object)
78-
} else {
79-
throw InterposeError.objectPosingAsDifferentClass(object, actualClass: actualClass)
80-
}
81-
}
103+
try Self.validateObjectForHooking(object)
82104

83105
// Only apply if a builder is present
84106
if let builder = builder {

Sources/InterposeKit/InterposeSubclass.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,16 @@ class InterposeSubclass {
6969
/// We need to reuse a dynamic subclass if the object already has one.
7070
private func getExistingSubclass() -> AnyClass? {
7171
let actualClass: AnyClass = object_getClass(object)!
72-
if NSStringFromClass(actualClass).hasPrefix(Constants.subclassSuffix) {
72+
if Self.isInterposeSubclass(actualClass) {
7373
return actualClass
7474
}
7575
return nil
7676
}
7777

78+
static func isInterposeSubclass(_ klass: AnyClass) -> Bool {
79+
NSStringFromClass(klass).hasPrefix(Constants.subclassSuffix)
80+
}
81+
7882
#if !os(Linux)
7983
private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) {
8084
// crashes on linux

Sources/InterposeKit/ObjectHook.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ extension Interpose {
1818
/// Initialize a new hook to interpose an instance method.
1919
public init(object: AnyObject, selector: Selector,
2020
implementation: (ObjectHook<MethodSignature, HookSignature>) -> HookSignature?) throws {
21+
try Interpose.validateObjectForHooking(object)
2122
self.object = object
2223
try super.init(class: type(of: object), selector: selector)
2324
let block = implementation(self) as AnyObject

Tests/InterposeKitTests/ObjectInterposeTests.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@ import XCTest
44

55
final class ObjectInterposeTests: InterposeKitTestCase {
66

7+
func testRejectsCoreFoundationBackedObject() throws {
8+
let url = try XCTUnwrap(NSURL(string: "https://www.google.com"))
9+
let expectedError = InterposeError.coreFoundationObjectDetected(url)
10+
11+
XCTAssertThrowsError(try Interpose(url)) { error in
12+
XCTAssertEqual(error as? InterposeError, expectedError)
13+
}
14+
XCTAssertThrowsError(try url.hook(
15+
#selector(getter: NSURL.host),
16+
methodSignature: (@convention(c) (AnyObject, Selector) -> String).self,
17+
hookSignature: (@convention(block) (AnyObject) -> String).self
18+
) { _ in
19+
{ _ in "www.facebook.com" }
20+
}) { error in
21+
XCTAssertEqual(error as? InterposeError, expectedError)
22+
}
23+
24+
XCTAssertEqual(url.host, "www.google.com")
25+
}
26+
727
func testInterposeSingleObject() throws {
828
let testObj = TestClass()
929
let testObj2 = TestClass()

0 commit comments

Comments
 (0)