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
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: StikJIT.xcframework
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.
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.

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 {
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

View file

@ -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)
}

View file

@ -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).")
}

View file

@ -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)
}
}

View file

@ -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):