Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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 changes: 98 additions & 0 deletions Resources/legacy.js
Original file line number Diff line number Diff line change
@@ -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);
4 changes: 2 additions & 2 deletions Sources/BundledScript.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Sources/JITSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() }

Expand All @@ -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)
}
Expand Down
8 changes: 5 additions & 3 deletions Sources/ScriptRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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).")
}
Expand Down
17 changes: 16 additions & 1 deletion Sources/StikJIT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
2 changes: 1 addition & 1 deletion Sources/StikJITError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading