Support for legacy script (#1)

This commit is contained in:
CelloSerenity 2026-06-29 05:10:34 -06:00 • committed by GitHub
parent 5bdecfcafa
commit 2320badda8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 128 additions and 13 deletions

View file

@ -50,7 +50,7 @@ jobs:
run: zip -ry StikJIT.xcframework.zip StikJIT.xcframework run: zip -ry StikJIT.xcframework.zip StikJIT.xcframework
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: StikJIT.xcframework name: StikJIT.xcframework
path: StikJIT.xcframework.zip path: StikJIT.xcframework.zip

View file

@ -2,7 +2,7 @@
An iOS XCFramework that enables JIT for another process over the device's RSD tunnel. 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 ## 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 ## Build
@ -35,4 +35,4 @@ xcodebuild -create-xcframework \
## License ## 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.

98
Resources/legacy.js Normal file
View file

@ -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:(?<tid>[0-9a-f]+);/.exec(brkResponse);
let tid = tidMatch ? tidMatch.groups['tid'] : null;
let pcMatch = /20:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
let pc = pcMatch ? pcMatch.groups['reg'] : null;
let x0Match = /00:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
let x0 = x0Match ? x0Match.groups['reg'] : null;
let x1Match = /01:(?<reg>[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);

View file

@ -2,9 +2,9 @@ import Foundation
enum BundledScript { enum BundledScript {
static func universalJS() throws -> String { static func source(for script: StikJIT.Script) throws -> String {
let bundle = Bundle(for: BundleToken.self) 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), let source = try? String(contentsOf: url, encoding: .utf8),
!source.isEmpty else { !source.isEmpty else {
throw StikJITError.scriptUnavailable throw StikJITError.scriptUnavailable

View file

@ -29,7 +29,7 @@ final class JITSession {
self.configuration = configuration 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() let tunnel = try makeTunnel()
defer { tunnel.free() } defer { tunnel.free() }
@ -43,7 +43,7 @@ final class JITSession {
debug_proxy_set_ack_mode(debugProxy, 0) debug_proxy_set_ack_mode(debugProxy, 0)
if ProcessInfo.processInfo.hasTXM { 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 { } else {
try attachWithoutScript(targetPID: targetPID, debugProxy: debugProxy, progress: progress) try attachWithoutScript(targetPID: targetPID, debugProxy: debugProxy, progress: progress)
} }

View file

@ -8,17 +8,19 @@ final class ScriptRunner {
private let targetPID: Int32 private let targetPID: Int32
private let debugProxy: OpaquePointer private let debugProxy: OpaquePointer
private let script: StikJIT.Script
private let progress: (String) -> Void private let progress: (String) -> Void
private var context: JSContext? 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.targetPID = targetPID
self.debugProxy = debugProxy self.debugProxy = debugProxy
self.script = script
self.progress = progress self.progress = progress
} }
func run() throws { func run() throws {
let source = try BundledScript.universalJS() let source = try BundledScript.source(for: script)
guard let context = JSContext() else { throw StikJITError.scriptUnavailable } guard let context = JSContext() else { throw StikJITError.scriptUnavailable }
self.context = context self.context = context
@ -45,7 +47,7 @@ final class ScriptRunner {
context.setObject(prepare, forKeyedSubscript: "prepare_memory_region" as NSString) context.setObject(prepare, forKeyedSubscript: "prepare_memory_region" as NSString)
context.setObject(log, forKeyedSubscript: "log" 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) context.evaluateScript(source)
progress("JIT script finished (region blessed, detached).") progress("JIT script finished (region blessed, detached).")
} }

View file

@ -2,6 +2,20 @@ import Foundation
public enum StikJIT { 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 struct Configuration: Sendable {
public var deviceAddress: String public var deviceAddress: String
@ -19,8 +33,9 @@ public enum StikJIT {
public static func enableJIT(targetPID: Int32, public static func enableJIT(targetPID: Int32,
pairingFile: URL, pairingFile: URL,
configuration: Configuration = .default, configuration: Configuration = .default,
script: Script = .universal,
progress: @escaping (String) -> Void = { _ in }) throws { progress: @escaping (String) -> Void = { _ in }) throws {
let session = JITSession(pairingFilePath: pairingFile.path, configuration: configuration) let session = JITSession(pairingFilePath: pairingFile.path, configuration: configuration)
try session.enableJIT(targetPID: targetPID, progress: progress) try session.enableJIT(targetPID: targetPID, script: script, progress: progress)
} }
} }

View file

@ -15,7 +15,7 @@ public enum StikJITError: Error, LocalizedError {
case .pairingFile(let detail): case .pairingFile(let detail):
return "Pairing file error: \(detail)." return "Pairing file error: \(detail)."
case .scriptUnavailable: 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: case .debugProxyUnavailable:
return "Failed to establish a debugserver connection to the target process." return "Failed to establish a debugserver connection to the target process."
case .device(let code, let subCode, let message): case .device(let code, let subCode, let message):