mirror of
https://github.com/StikDebug/StikJIT.git
synced 2026-10-15 12:02:15 +00:00
Support for legacy script (#1)
This commit is contained in:
parent
5bdecfcafa
commit
2320badda8
8 changed files with 128 additions and 13 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
98
Resources/legacy.js
Normal 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);
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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).")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue