diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5ca03b9801..2d4343d241 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,7 +50,7 @@ jobs: run: zip -ry StikJIT.xcframework.zip StikJIT.xcframework - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: StikJIT.xcframework path: StikJIT.xcframework.zip diff --git a/README.md b/README.md index 924ad0fbee..28cdaa55ad 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ An iOS XCFramework that enables JIT for another process over the device's RSD tunnel. -JIT cannot be enabled in-process, since a process that attaches a debugger to itself deadlocks. StikJIT runs in a separate process, attaches a debugserver to the target by PID, enables JIT for it, then detaches. It is self-contained: the idevice FFI and `universal.js` are bundled inside. The target must have the `get-task-allow` entitlement. +JIT cannot be enabled in-process, since a process that attaches a debugger to itself deadlocks. StikJIT runs in a separate process, attaches a debugserver to the target by PID, enables JIT for it, then detaches. It is self-contained: the idevice FFI and its bundled JIT scripts (`universal.js`, `legacy.js`) are bundled inside. The target must have the `get-task-allow` entitlement. ## Use @@ -20,7 +20,7 @@ try StikJIT.enableJIT( ) ``` -It blocks until done and throws `StikJITError` on failure. Pass `configuration:` to override the tunnel endpoint (defaults to `10.7.0.1:49152`). +It blocks until done and throws `StikJITError` on failure. Pass `configuration:` to override the tunnel endpoint (defaults to `10.7.0.1:49152`). Pass `script:` to select the bundled JS used to drive the JIT-enabling exchange on devices with TXM — `.universal` (default) or `.legacy` based on your app's needs. ## Build @@ -35,4 +35,4 @@ xcodebuild -create-xcframework \ ## License -StikJIT is licensed under the MPL-2.0 (see [`LICENSE`](LICENSE)). It uses StikDebug as a reference, with the bundled [idevice](https://github.com/jkcoxson/idevice) and universal.js retaining their own licenses. +StikJIT is licensed under the MPL-2.0 (see [`LICENSE`](LICENSE)). It uses StikDebug as a reference, with the bundled [idevice](https://github.com/jkcoxson/idevice), universal.js, and legacy.js retaining their own licenses. diff --git a/Resources/legacy.js b/Resources/legacy.js new file mode 100644 index 0000000000..090544ca89 --- /dev/null +++ b/Resources/legacy.js @@ -0,0 +1,98 @@ +function littleEndianHexStringToNumber(hexStr) { + const bytes = []; + for (let i = 0; i < hexStr.length; i += 2) { + bytes.push(parseInt(hexStr.substr(i, 2), 16)); + } + let num = 0n; + for (let i = 4; i >= 0; i--) { + num = (num << 8n) | BigInt(bytes[i]); + } + return num; +} + +function numberToLittleEndianHexString(num) { + const bytes = []; + for (let i = 0; i < 5; i++) { + bytes.push(Number(num & 0xFFn)); + num >>= 8n; + } + while (bytes.length < 8) { + bytes.push(0); + } + return bytes.map(b => b.toString(16).padStart(2, '0')).join(''); +} + +function littleEndianHexToU32(hexStr) { + return parseInt(hexStr.match(/../g).reverse().join(''), 16); +} + +function extractBrkImmediate(u32) { + return (u32 >> 5) & 0xFFFF; +} + +function attach(breakpointcount) { + let pid = get_pid(); + log(`pid = ${pid}`); + let attachResponse = send_command(`vAttach;${pid.toString(16)}`); + log(`attach_response = ${attachResponse}`); + + let validBreakpoints = 0; + let totalBreakpoints = 0; + + while (validBreakpoints < breakpointcount) { + totalBreakpoints++; + log(`Handling breakpoint ${totalBreakpoints} (looking for valid breakpoint ${validBreakpoints + 1}/${breakpointcount})`); + + let brkResponse = send_command(`c`); + log(`brkResponse = ${brkResponse}`); + + let tidMatch = /T[0-9a-f]+thread:(?[0-9a-f]+);/.exec(brkResponse); + let tid = tidMatch ? tidMatch.groups['tid'] : null; + let pcMatch = /20:(?[0-9a-f]{16});/.exec(brkResponse); + let pc = pcMatch ? pcMatch.groups['reg'] : null; + let x0Match = /00:(?[0-9a-f]{16});/.exec(brkResponse); + let x0 = x0Match ? x0Match.groups['reg'] : null; + let x1Match = /01:(?[0-9a-f]{16});/.exec(brkResponse); + let x1 = x1Match ? x1Match.groups['reg'] : null; + + if (!tid || !pc || !x0 || !x1) { + log(`Failed to extract registers: tid=${tid}, pc=${pc}, x0=${x0}, x1=${x1}`); + continue; + } + + const pcNum = littleEndianHexStringToNumber(pc); + const x0Num = littleEndianHexStringToNumber(x0); + const x1Num = littleEndianHexStringToNumber(x1); + log(`tid = ${tid}, pc = ${pcNum.toString(16)}, x0 = ${x0Num.toString(16)}, x1 = ${x1Num.toString(16)}`); + + let instructionResponse = send_command(`m${pcNum.toString(16)},4`); + log(`instruction at pc: ${instructionResponse}`); + let instrU32 = littleEndianHexToU32(instructionResponse); + let brkImmediate = extractBrkImmediate(instrU32); + log(`BRK immediate: 0x${brkImmediate.toString(16)} (${brkImmediate})`); + + if (brkImmediate !== 0x69) { + log(`Skipping breakpoint: brk immediate was not 0x69 (was 0x${brkImmediate.toString(16)})`); + continue; + } + + log(`BRK immediate matches expected value 0x69 - processing valid breakpoint ${validBreakpoints + 1}/${breakpointcount}`); + + log(`Allocated JIT page at address: 0x${x0Num.toString(16)}`); + + let prepareJITPageResponse = prepare_memory_region(x0Num, x1Num); + log(`prepareJITPageResponse = ${prepareJITPageResponse}`); + + let pcPlus4 = numberToLittleEndianHexString(pcNum + 4n); + let pcPlus4Response = send_command(`P20=${pcPlus4};thread:${tid};`); + log(`pcPlus4Response = ${pcPlus4Response}`); + + validBreakpoints++; + log(`Completed valid breakpoint ${validBreakpoints}/${breakpointcount}`); + } + + let detachResponse = send_command(`D`); + log(`detachResponse = ${detachResponse}`); +} + +attach(1); \ No newline at end of file diff --git a/Sources/BundledScript.swift b/Sources/BundledScript.swift index 80df5b4024..31910f9797 100644 --- a/Sources/BundledScript.swift +++ b/Sources/BundledScript.swift @@ -2,9 +2,9 @@ import Foundation enum BundledScript { - static func universalJS() throws -> String { + static func source(for script: StikJIT.Script) throws -> String { let bundle = Bundle(for: BundleToken.self) - guard let url = bundle.url(forResource: "universal", withExtension: "js"), + guard let url = bundle.url(forResource: script.resourceName, withExtension: "js"), let source = try? String(contentsOf: url, encoding: .utf8), !source.isEmpty else { throw StikJITError.scriptUnavailable diff --git a/Sources/JITSession.swift b/Sources/JITSession.swift index 3648cb8408..2da1eddb16 100644 --- a/Sources/JITSession.swift +++ b/Sources/JITSession.swift @@ -29,7 +29,7 @@ final class JITSession { self.configuration = configuration } - func enableJIT(targetPID: Int32, progress: @escaping (String) -> Void) throws { + func enableJIT(targetPID: Int32, script: StikJIT.Script, progress: @escaping (String) -> Void) throws { let tunnel = try makeTunnel() defer { tunnel.free() } @@ -43,7 +43,7 @@ final class JITSession { debug_proxy_set_ack_mode(debugProxy, 0) if ProcessInfo.processInfo.hasTXM { - try ScriptRunner(targetPID: targetPID, debugProxy: debugProxy, progress: progress).run() + try ScriptRunner(targetPID: targetPID, debugProxy: debugProxy, script: script, progress: progress).run() } else { try attachWithoutScript(targetPID: targetPID, debugProxy: debugProxy, progress: progress) } diff --git a/Sources/ScriptRunner.swift b/Sources/ScriptRunner.swift index 13e81c7945..687e8e0a52 100644 --- a/Sources/ScriptRunner.swift +++ b/Sources/ScriptRunner.swift @@ -8,17 +8,19 @@ final class ScriptRunner { private let targetPID: Int32 private let debugProxy: OpaquePointer + private let script: StikJIT.Script private let progress: (String) -> Void private var context: JSContext? - init(targetPID: Int32, debugProxy: OpaquePointer, progress: @escaping (String) -> Void) { + init(targetPID: Int32, debugProxy: OpaquePointer, script: StikJIT.Script, progress: @escaping (String) -> Void) { self.targetPID = targetPID self.debugProxy = debugProxy + self.script = script self.progress = progress } func run() throws { - let source = try BundledScript.universalJS() + let source = try BundledScript.source(for: script) guard let context = JSContext() else { throw StikJITError.scriptUnavailable } self.context = context @@ -45,7 +47,7 @@ final class ScriptRunner { context.setObject(prepare, forKeyedSubscript: "prepare_memory_region" as NSString) context.setObject(log, forKeyedSubscript: "log" as NSString) - progress("Running universal.js against pid \(targetPID)…") + progress("Running \(script.resourceName).js against pid \(targetPID)…") context.evaluateScript(source) progress("JIT script finished (region blessed, detached).") } diff --git a/Sources/StikJIT.swift b/Sources/StikJIT.swift index 368b13cdda..59adae4404 100644 --- a/Sources/StikJIT.swift +++ b/Sources/StikJIT.swift @@ -2,6 +2,20 @@ import Foundation public enum StikJIT { + public enum Script: Sendable { + + case universal + + case legacy + + var resourceName: String { + switch self { + case .universal: return "universal" + case .legacy: return "legacy" + } + } + } + public struct Configuration: Sendable { public var deviceAddress: String @@ -19,8 +33,9 @@ public enum StikJIT { public static func enableJIT(targetPID: Int32, pairingFile: URL, configuration: Configuration = .default, + script: Script = .universal, progress: @escaping (String) -> Void = { _ in }) throws { let session = JITSession(pairingFilePath: pairingFile.path, configuration: configuration) - try session.enableJIT(targetPID: targetPID, progress: progress) + try session.enableJIT(targetPID: targetPID, script: script, progress: progress) } } diff --git a/Sources/StikJITError.swift b/Sources/StikJITError.swift index 17dac40bed..d99926c10f 100644 --- a/Sources/StikJITError.swift +++ b/Sources/StikJITError.swift @@ -15,7 +15,7 @@ public enum StikJITError: Error, LocalizedError { case .pairingFile(let detail): return "Pairing file error: \(detail)." case .scriptUnavailable: - return "The bundled universal.js could not be loaded from the StikJIT framework." + return "The bundled JIT script could not be loaded from the StikJIT framework." case .debugProxyUnavailable: return "Failed to establish a debugserver connection to the target process." case .device(let code, let subCode, let message):