Skip to content

Commit 4bcf98e

Browse files
authored
Refactor: Extract InterposeSubclass and HookFinder (#17)
1 parent 17853ad commit 4bcf98e

File tree

4 files changed

+190
-161
lines changed

4 files changed

+190
-161
lines changed

InterposeKit.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
780FC9FA249822C900DA5A14 /* HookFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780FC9F9249822C900DA5A14 /* HookFinder.swift */; };
1011
7810959E248D43DC008A943C /* ClassHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7810959D248D43DC008A943C /* ClassHook.swift */; };
1112
781095A0248D50C1008A943C /* Watcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7810959F248D50C1008A943C /* Watcher.swift */; };
1213
781095A8248D6DFB008A943C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781095A7248D6DFB008A943C /* AppDelegate.swift */; };
@@ -26,6 +27,7 @@
2627
78C5A4A82494D75100EE9756 /* MultipleInterposing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C5A4A72494D75100EE9756 /* MultipleInterposing.swift */; };
2728
78E20D952497B3480021552C /* ITKSuperBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 78E20D922497B3470021552C /* ITKSuperBuilder.h */; };
2829
78E20D962497B3480021552C /* ITKSuperBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 78E20D942497B3470021552C /* ITKSuperBuilder.m */; };
30+
78E20D9824981B2A0021552C /* InterposeSubclass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E20D9724981B2A0021552C /* InterposeSubclass.swift */; };
2931
78EDB8DA248BA9B300D2F6C1 /* TestClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */; };
3032
78EDB8DB248BA9BB00D2F6C1 /* ObjectInterposeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */; };
3133
78EDB8DD248BAA5600D2F6C1 /* AnyHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */; };
@@ -65,6 +67,7 @@
6567
/* End PBXCopyFilesBuildPhase section */
6668

6769
/* Begin PBXFileReference section */
70+
780FC9F9249822C900DA5A14 /* HookFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HookFinder.swift; path = Sources/InterposeKit/HookFinder.swift; sourceTree = SOURCE_ROOT; };
6871
7810959D248D43DC008A943C /* ClassHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ClassHook.swift; path = Sources/InterposeKit/ClassHook.swift; sourceTree = SOURCE_ROOT; };
6972
7810959F248D50C1008A943C /* Watcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Watcher.swift; path = Sources/InterposeKit/Watcher.swift; sourceTree = SOURCE_ROOT; };
7073
781095A5248D6DFB008A943C /* InterposeTestHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InterposeTestHost.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -92,6 +95,7 @@
9295
78C5A4A72494D75100EE9756 /* MultipleInterposing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MultipleInterposing.swift; path = Tests/InterposeKitTests/MultipleInterposing.swift; sourceTree = SOURCE_ROOT; };
9396
78E20D922497B3470021552C /* ITKSuperBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ITKSuperBuilder.h; sourceTree = "<group>"; };
9497
78E20D942497B3470021552C /* ITKSuperBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ITKSuperBuilder.m; sourceTree = "<group>"; };
98+
78E20D9724981B2A0021552C /* InterposeSubclass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeSubclass.swift; path = Sources/InterposeKit/InterposeSubclass.swift; sourceTree = SOURCE_ROOT; };
9599
78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TestClass.swift; path = Tests/InterposeKitTests/TestClass.swift; sourceTree = SOURCE_ROOT; };
96100
78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ObjectInterposeTests.swift; path = Tests/InterposeKitTests/ObjectInterposeTests.swift; sourceTree = SOURCE_ROOT; };
97101
78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AnyHook.swift; path = Sources/InterposeKit/AnyHook.swift; sourceTree = SOURCE_ROOT; };
@@ -182,6 +186,8 @@
182186
78EDB8FE248D0A9900D2F6C1 /* ObjectHook.swift */,
183187
78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */,
184188
7810959F248D50C1008A943C /* Watcher.swift */,
189+
78E20D9724981B2A0021552C /* InterposeSubclass.swift */,
190+
780FC9F9249822C900DA5A14 /* HookFinder.swift */,
185191
);
186192
path = InterposeKit;
187193
sourceTree = "<group>";
@@ -391,13 +397,15 @@
391397
isa = PBXSourcesBuildPhase;
392398
buildActionMask = 2147483647;
393399
files = (
400+
780FC9FA249822C900DA5A14 /* HookFinder.swift in Sources */,
394401
781095A0248D50C1008A943C /* Watcher.swift in Sources */,
395402
78A2F26E2496B54B00F5AC5F /* InterposeError.swift in Sources */,
396403
78E20D962497B3480021552C /* ITKSuperBuilder.m in Sources */,
397404
7810959E248D43DC008A943C /* ClassHook.swift in Sources */,
398405
78C39D912483165600B46395 /* InterposeKit.swift in Sources */,
399406
78EDB8FF248D0A9900D2F6C1 /* ObjectHook.swift in Sources */,
400407
78EDB903248D42CD00D2F6C1 /* LinuxCompileSupport.swift in Sources */,
408+
78E20D9824981B2A0021552C /* InterposeSubclass.swift in Sources */,
401409
78EDB8DD248BAA5600D2F6C1 /* AnyHook.swift in Sources */,
402410
);
403411
runOnlyForDeploymentPostprocessing = 0;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import Foundation
2+
3+
extension Interpose {
4+
5+
private struct AssociatedKeys {
6+
static var hookForBlock: UInt8 = 0
7+
}
8+
9+
private class WeakObjectContainer<T: AnyObject>: NSObject {
10+
private weak var _object: T?
11+
12+
var object: T? {
13+
return _object
14+
}
15+
init(with object: T?) {
16+
_object = object
17+
}
18+
}
19+
20+
static func storeHook<HookType: AnyHook>(hook: HookType, to block: AnyObject) {
21+
// Weakly store reference to hook inside the block of the IMP.
22+
objc_setAssociatedObject(block, &AssociatedKeys.hookForBlock, WeakObjectContainer(with: hook), .OBJC_ASSOCIATION_RETAIN)
23+
24+
}
25+
26+
// Finds the hook to a given implementation.
27+
static func hookForIMP<HookType: AnyHook>(_ imp: IMP) -> HookType? {
28+
// Get the block that backs our IMP replacement
29+
guard let block = imp_getBlock(imp) else { return nil }
30+
let container = objc_getAssociatedObject(block, &AssociatedKeys.hookForBlock) as? WeakObjectContainer<HookType>
31+
return container?.object
32+
}
33+
34+
// Find the hook above us (not necessarily topmost)
35+
static func findNextHook<HookType: AnyHook>(selfHook: HookType, topmostIMP: IMP) -> HookType? {
36+
// We are not topmost hook, so find the hook above us!
37+
var impl: IMP? = topmostIMP
38+
var currentHook: HookType?
39+
repeat {
40+
// get topmost hook
41+
let hook: HookType? = Interpose.hookForIMP(impl!)
42+
if hook === selfHook {
43+
// return parent
44+
return currentHook
45+
}
46+
// crawl down the chain until we find ourselves
47+
currentHook = hook
48+
impl = hook?.origIMP
49+
} while impl != nil
50+
return nil
51+
}
52+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import Foundation
2+
3+
class InterposeSubclass {
4+
5+
private enum Constants {
6+
static let subclassSuffix = "InterposeKit_"
7+
}
8+
9+
enum ObjCSelector {
10+
static let getClass = Selector((("class")))
11+
}
12+
13+
enum ObjCMethodEncoding {
14+
static let getClass = extract("#@:")
15+
16+
private static func extract(_ string: StaticString) -> UnsafePointer<CChar> {
17+
return UnsafeRawPointer(string.utf8Start).assumingMemoryBound(to: CChar.self)
18+
}
19+
}
20+
21+
/// The object that is being hooked.
22+
let object: AnyObject
23+
24+
/// Subclass that we create on the fly
25+
private(set) var dynamicClass: AnyClass
26+
27+
/// If the class has been altered (e.g. via NSKVONotifying_ KVO logic)
28+
/// then perceived and actual class don't match.
29+
///
30+
/// Making KVO and Object-based hooking work at the same time is difficult.
31+
/// If we make a dynamic subclass over KVO, invalidating the token crashes in cache_getImp.
32+
init(object: AnyObject) throws {
33+
self.object = object
34+
dynamicClass = type(of: object) // satisfy set to something
35+
dynamicClass = try getExistingSubclass() ?? createSubclass()
36+
}
37+
38+
private func createSubclass() throws -> AnyClass {
39+
let perceivedClass: AnyClass = type(of: object)
40+
let actualClass: AnyClass = object_getClass(object)!
41+
42+
let className = NSStringFromClass(perceivedClass)
43+
// Right now we are wasteful. Might be able to optimize for shared IMP?
44+
let uuid = UUID().uuidString.replacingOccurrences(of: "-", with: "")
45+
let subclassName = Constants.subclassSuffix + className + uuid
46+
47+
let subclass: AnyClass? = subclassName.withCString { cString in
48+
// swiftlint:disable:next force_cast
49+
if let existingClass = objc_getClass(cString) as! AnyClass? {
50+
return existingClass
51+
} else {
52+
guard let subclass: AnyClass = objc_allocateClassPair(actualClass, cString, 0) else { return nil }
53+
replaceGetClass(in: subclass, decoy: perceivedClass)
54+
objc_registerClassPair(subclass)
55+
return subclass
56+
}
57+
}
58+
59+
guard let nonnullSubclass = subclass else {
60+
throw InterposeError.failedToAllocateClassPair(class: perceivedClass, subclassName: subclassName)
61+
}
62+
63+
object_setClass(object, nonnullSubclass)
64+
Interpose.log("Generated \(NSStringFromClass(nonnullSubclass)) for object (was: \(NSStringFromClass(class_getSuperclass(object_getClass(object)!)!)))")
65+
return nonnullSubclass
66+
}
67+
68+
/// We need to reuse a dynamic subclass if the object already has one.
69+
private func getExistingSubclass() -> AnyClass? {
70+
let actualClass: AnyClass = object_getClass(object)!
71+
if NSStringFromClass(actualClass).hasPrefix(Constants.subclassSuffix) {
72+
return actualClass
73+
}
74+
return nil
75+
}
76+
77+
#if !os(Linux)
78+
private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) {
79+
// crashes on linux
80+
let getClass: @convention(block) (AnyObject) -> AnyClass = { _ in
81+
perceivedClass
82+
}
83+
let impl = imp_implementationWithBlock(getClass as Any)
84+
_ = class_replaceMethod(`class`, ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass)
85+
_ = class_replaceMethod(object_getClass(`class`), ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass)
86+
}
87+
88+
class var supportsSuperTrampolines: Bool {
89+
NSClassFromString("SuperBuilder")?.value(forKey: "isSupportedArchitecure") as? Bool ?? false
90+
}
91+
92+
private lazy var addSuperImpl: @convention(c) (AnyClass, Selector, NSErrorPointer) -> Bool = {
93+
let handle = dlopen(nil, RTLD_LAZY)
94+
let imp = dlsym(handle, "IKTAddSuperImplementationToClass")
95+
return unsafeBitCast(imp, to: (@convention(c) (AnyClass, Selector, NSErrorPointer) -> Bool).self)
96+
}()
97+
98+
func addSuperTrampoline(selector: Selector) {
99+
var error: NSError?
100+
if addSuperImpl(dynamicClass, selector, &error) == false {
101+
Interpose.log("Failed to add super implementation to -[\(dynamicClass).\(selector)]: \(error!)")
102+
} else {
103+
let imp = class_getMethodImplementation(dynamicClass, selector)!
104+
Interpose.log("Added super for -[\(dynamicClass).\(selector)]: \(imp)")
105+
}
106+
}
107+
#else
108+
func addSuperTrampoline(selector: Selector) { }
109+
class var supportsSuperTrampolines: Bool { return false }
110+
private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) {}
111+
#endif
112+
}

0 commit comments

Comments
 (0)