Skip to content

Commit 67f36da

Browse files
committed
legacy support
1 parent 5bdecfc commit 67f36da

8 files changed

Lines changed: 128 additions & 13 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
run: zip -ry StikJIT.xcframework.zip StikJIT.xcframework
5151

5252
- name: Upload artifact
53-
uses: actions/upload-artifact@v4
53+
uses: actions/upload-artifact@v7
5454
with:
5555
name: StikJIT.xcframework
5656
path: StikJIT.xcframework.zip

‎README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
An iOS XCFramework that enables JIT for another process over the device's RSD tunnel.
44

5-
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.
5+
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.
66

77
## Use
88

@@ -20,7 +20,7 @@ try StikJIT.enableJIT(
2020
)
2121
```
2222

23-
It blocks until done and throws `StikJITError` on failure. Pass `configuration:` to override the tunnel endpoint (defaults to `10.7.0.1:49152`).
23+
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.
2424

2525
## Build
2626

@@ -35,4 +35,4 @@ xcodebuild -create-xcframework \
3535

3636
## License
3737

38-
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.
38+
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.

‎Resources/legacy.js‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
function littleEndianHexStringToNumber(hexStr) {
2+
const bytes = [];
3+
for (let i = 0; i < hexStr.length; i += 2) {
4+
bytes.push(parseInt(hexStr.substr(i, 2), 16));
5+
}
6+
let num = 0n;
7+
for (let i = 4; i >= 0; i--) {
8+
num = (num << 8n) | BigInt(bytes[i]);
9+
}
10+
return num;
11+
}
12+
13+
function numberToLittleEndianHexString(num) {
14+
const bytes = [];
15+
for (let i = 0; i < 5; i++) {
16+
bytes.push(Number(num & 0xFFn));
17+
num >>= 8n;
18+
}
19+
while (bytes.length < 8) {
20+
bytes.push(0);
21+
}
22+
return bytes.map(b => b.toString(16).padStart(2, '0')).join('');
23+
}
24+
25+
function littleEndianHexToU32(hexStr) {
26+
return parseInt(hexStr.match(/../g).reverse().join(''), 16);
27+
}
28+
29+
function extractBrkImmediate(u32) {
30+
return (u32 >> 5) & 0xFFFF;
31+
}
32+
33+
function attach(breakpointcount) {
34+
let pid = get_pid();
35+
log(`pid = ${pid}`);
36+
let attachResponse = send_command(`vAttach;${pid.toString(16)}`);
37+
log(`attach_response = ${attachResponse}`);
38+
39+
let validBreakpoints = 0;
40+
let totalBreakpoints = 0;
41+
42+
while (validBreakpoints < breakpointcount) {
43+
totalBreakpoints++;
44+
log(`Handling breakpoint ${totalBreakpoints} (looking for valid breakpoint ${validBreakpoints + 1}/${breakpointcount})`);
45+
46+
let brkResponse = send_command(`c`);
47+
log(`brkResponse = ${brkResponse}`);
48+
49+
let tidMatch = /T[0-9a-f]+thread:(?<tid>[0-9a-f]+);/.exec(brkResponse);
50+
let tid = tidMatch ? tidMatch.groups['tid'] : null;
51+
let pcMatch = /20:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
52+
let pc = pcMatch ? pcMatch.groups['reg'] : null;
53+
let x0Match = /00:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
54+
let x0 = x0Match ? x0Match.groups['reg'] : null;
55+
let x1Match = /01:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
56+
let x1 = x1Match ? x1Match.groups['reg'] : null;
57+
58+
if (!tid || !pc || !x0 || !x1) {
59+
log(`Failed to extract registers: tid=${tid}, pc=${pc}, x0=${x0}, x1=${x1}`);
60+
continue;
61+
}
62+
63+
const pcNum = littleEndianHexStringToNumber(pc);
64+
const x0Num = littleEndianHexStringToNumber(x0);
65+
const x1Num = littleEndianHexStringToNumber(x1);
66+
log(`tid = ${tid}, pc = ${pcNum.toString(16)}, x0 = ${x0Num.toString(16)}, x1 = ${x1Num.toString(16)}`);
67+
68+
let instructionResponse = send_command(`m${pcNum.toString(16)},4`);
69+
log(`instruction at pc: ${instructionResponse}`);
70+
let instrU32 = littleEndianHexToU32(instructionResponse);
71+
let brkImmediate = extractBrkImmediate(instrU32);
72+
log(`BRK immediate: 0x${brkImmediate.toString(16)} (${brkImmediate})`);
73+
74+
if (brkImmediate !== 0x69) {
75+
log(`Skipping breakpoint: brk immediate was not 0x69 (was 0x${brkImmediate.toString(16)})`);
76+
continue;
77+
}
78+
79+
log(`BRK immediate matches expected value 0x69 - processing valid breakpoint ${validBreakpoints + 1}/${breakpointcount}`);
80+
81+
log(`Allocated JIT page at address: 0x${x0Num.toString(16)}`);
82+
83+
let prepareJITPageResponse = prepare_memory_region(x0Num, x1Num);
84+
log(`prepareJITPageResponse = ${prepareJITPageResponse}`);
85+
86+
let pcPlus4 = numberToLittleEndianHexString(pcNum + 4n);
87+
let pcPlus4Response = send_command(`P20=${pcPlus4};thread:${tid};`);
88+
log(`pcPlus4Response = ${pcPlus4Response}`);
89+
90+
validBreakpoints++;
91+
log(`Completed valid breakpoint ${validBreakpoints}/${breakpointcount}`);
92+
}
93+
94+
let detachResponse = send_command(`D`);
95+
log(`detachResponse = ${detachResponse}`);
96+
}
97+
98+
attach(1);

‎Sources/BundledScript.swift‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import Foundation
22

33
enum BundledScript {
44

5-
static func universalJS() throws -> String {
5+
static func source(for script: StikJIT.Script) throws -> String {
66
let bundle = Bundle(for: BundleToken.self)
7-
guard let url = bundle.url(forResource: "universal", withExtension: "js"),
7+
guard let url = bundle.url(forResource: script.resourceName, withExtension: "js"),
88
let source = try? String(contentsOf: url, encoding: .utf8),
99
!source.isEmpty else {
1010
throw StikJITError.scriptUnavailable

‎Sources/JITSession.swift‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ final class JITSession {
2929
self.configuration = configuration
3030
}
3131

32-
func enableJIT(targetPID: Int32, progress: @escaping (String) -> Void) throws {
32+
func enableJIT(targetPID: Int32, script: StikJIT.Script, progress: @escaping (String) -> Void) throws {
3333
let tunnel = try makeTunnel()
3434
defer { tunnel.free() }
3535

@@ -43,7 +43,7 @@ final class JITSession {
4343
debug_proxy_set_ack_mode(debugProxy, 0)
4444

4545
if ProcessInfo.processInfo.hasTXM {
46-
try ScriptRunner(targetPID: targetPID, debugProxy: debugProxy, progress: progress).run()
46+
try ScriptRunner(targetPID: targetPID, debugProxy: debugProxy, script: script, progress: progress).run()
4747
} else {
4848
try attachWithoutScript(targetPID: targetPID, debugProxy: debugProxy, progress: progress)
4949
}

‎Sources/ScriptRunner.swift‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ final class ScriptRunner {
88

99
private let targetPID: Int32
1010
private let debugProxy: OpaquePointer
11+
private let script: StikJIT.Script
1112
private let progress: (String) -> Void
1213
private var context: JSContext?
1314

14-
init(targetPID: Int32, debugProxy: OpaquePointer, progress: @escaping (String) -> Void) {
15+
init(targetPID: Int32, debugProxy: OpaquePointer, script: StikJIT.Script, progress: @escaping (String) -> Void) {
1516
self.targetPID = targetPID
1617
self.debugProxy = debugProxy
18+
self.script = script
1719
self.progress = progress
1820
}
1921

2022
func run() throws {
21-
let source = try BundledScript.universalJS()
23+
let source = try BundledScript.source(for: script)
2224

2325
guard let context = JSContext() else { throw StikJITError.scriptUnavailable }
2426
self.context = context
@@ -45,7 +47,7 @@ final class ScriptRunner {
4547
context.setObject(prepare, forKeyedSubscript: "prepare_memory_region" as NSString)
4648
context.setObject(log, forKeyedSubscript: "log" as NSString)
4749

48-
progress("Running universal.js against pid \(targetPID)…")
50+
progress("Running \(script.resourceName).js against pid \(targetPID)…")
4951
context.evaluateScript(source)
5052
progress("JIT script finished (region blessed, detached).")
5153
}

‎Sources/StikJIT.swift‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@ import Foundation
22

33
public enum StikJIT {
44

5+
public enum Script: Sendable {
6+
7+
case universal
8+
9+
case legacy
10+
11+
var resourceName: String {
12+
switch self {
13+
case .universal: return "universal"
14+
case .legacy: return "legacy"
15+
}
16+
}
17+
}
18+
519
public struct Configuration: Sendable {
620

721
public var deviceAddress: String
@@ -19,8 +33,9 @@ public enum StikJIT {
1933
public static func enableJIT(targetPID: Int32,
2034
pairingFile: URL,
2135
configuration: Configuration = .default,
36+
script: Script = .universal,
2237
progress: @escaping (String) -> Void = { _ in }) throws {
2338
let session = JITSession(pairingFilePath: pairingFile.path, configuration: configuration)
24-
try session.enableJIT(targetPID: targetPID, progress: progress)
39+
try session.enableJIT(targetPID: targetPID, script: script, progress: progress)
2540
}
2641
}

‎Sources/StikJITError.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public enum StikJITError: Error, LocalizedError {
1515
case .pairingFile(let detail):
1616
return "Pairing file error: \(detail)."
1717
case .scriptUnavailable:
18-
return "The bundled universal.js could not be loaded from the StikJIT framework."
18+
return "The bundled JIT script could not be loaded from the StikJIT framework."
1919
case .debugProxyUnavailable:
2020
return "Failed to establish a debugserver connection to the target process."
2121
case .device(let code, let subCode, let message):

0 commit comments

Comments
 (0)