Skip to content

Instantly share code, notes, and snippets.

@steipete
Last active May 28, 2022 12:21
Show Gist options
  • Save steipete/f955aaa0742021af15add0133d8482b9 to your computer and use it in GitHub Desktop.
Save steipete/f955aaa0742021af15add0133d8482b9 to your computer and use it in GitHub Desktop.
//
// Copyright © 2020 Peter Steinberger. MIT Licensed.
//
import Foundation
/// Text input can crash in Mac Catalyst due to a property not being atomic.
/// We swizzle the `documentState` property of `RTIInputSystemSession` to make it thread safe.
func installMacCatalystAppKitTextCrashFix() {
// RTIInputSystemSession is loaded later, so we will check a few times.
_dyld_register_func_for_add_image { _, _ in
DispatchQueue.main.async {
if fixMacCatalystInputSystemSessionRace() {
print("Successfully installed Mac Catalyst Text Input Race Fix.")
}
}
}
}
private var didInstallCrashFix = false
/// https://github.com/w0lfschild/macOS_headers/blob/master/macOS/PrivateFrameworks/RemoteTextInput/2399/RTIInputSystemSession.h
/// Sample crashers:
/// https://gist.github.com/steipete/504e79558d861211a3a9ff794e09c817
/// https://gist.github.com/steipete/7a125b20cce461bf9a072dfacd805507
private func fixMacCatalystInputSystemSessionRace() -> Bool {
guard let klass = NSClassFromString("RTIInputSystemSession") else { return false }
guard didInstallCrashFix == false else { return false }
var lockStore = os_unfair_lock()
let sel = NSSelectorFromString("documentState")
var origIMP : IMP? = nil
let newHandler: @convention(block) (AnyObject) -> AnyObject = { blockSelf in
typealias ClosureType = @convention(c) (AnyObject, Selector) -> AnyObject
let callableIMP = unsafeBitCast(origIMP, to: ClosureType.self)
os_unfair_lock_lock(&lockStore)
defer {
os_unfair_lock_unlock(&lockStore)
}
return callableIMP(blockSelf, sel)
}
guard let method = class_getInstanceMethod(klass, sel) else { return false }
origIMP = class_replaceMethod(klass, sel, imp_implementationWithBlock(newHandler), method_getTypeEncoding(method))
let setSel = NSSelectorFromString("setDocumentState:")
var setOrigIMP : IMP? = nil
let newSetHandler: @convention(block) (AnyObject, AnyObject) -> Void = { blockSelf, newValue in
typealias ClosureType = @convention(c) (AnyObject, Selector, AnyObject) -> Void
let callableIMP = unsafeBitCast(setOrigIMP, to: ClosureType.self)
os_unfair_lock_lock(&lockStore)
callableIMP(blockSelf, sel, newValue)
os_unfair_lock_unlock(&lockStore)
}
guard let setMethod = class_getInstanceMethod(klass, setSel) else { return false }
setOrigIMP = class_replaceMethod(klass, setSel, imp_implementationWithBlock(newSetHandler), method_getTypeEncoding(setMethod))
didInstallCrashFix = true
return origIMP != nil && setOrigIMP != nil
}
// Version using InterposeKit: https://github.com/steipete/InterposeKit
private func fixMacCatalystInputSystemSessionRace() {
do {
// Better to separate this string for static analysis
try Interpose.whenAvailable(["RTIInput", "SystemSession"]) {
let lock = DispatchQueue(label: "com.steipete.document-state-hack")
try $0.hook("documentState", { store in { `self` in
lock.sync {
store((@convention(c) (AnyObject, Selector) -> AnyObject).self)(`self`, store.selector)
}} as @convention(block) (AnyObject) -> AnyObject})
try $0.hook("setDocumentState:", { store in { `self`, newValue in
lock.sync {
store((@convention(c) (AnyObject, Selector, AnyObject) -> Void).self)(`self`, store.selector, newValue)
}} as @convention(block) (AnyObject, AnyObject) -> Void})
}
} catch {
print("Failed to fix input system: \(error).")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment