mirror of
https://github.com/StikDebug/StikJIT.git
synced 2026-10-16 04:20:30 +00:00
Compare commits
10 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32287268fa | ||
|
|
5d732f94b8 | ||
|
|
ee457f2f40 | ||
|
|
ef4e105cd7 | ||
|
|
4ba4072748 | ||
|
|
e6bbfe0399 | ||
|
|
4e08cecab9 | ||
|
|
709898d500 | ||
|
|
9ba3f85890 | ||
|
|
3623e72587 |
11 changed files with 3909 additions and 143 deletions
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
|
@ -14,7 +14,7 @@ permissions:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build XCFramework
|
name: Build XCFramework
|
||||||
runs-on: macos-15
|
runs-on: xcode-27
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
@ -58,7 +58,7 @@ jobs:
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
with:
|
with:
|
||||||
files: StikJIT.xcframework.zip
|
files: StikJIT.xcframework.zip
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ This guide separates two different jobs:
|
||||||
- **Part 1: Add iOS 26 JIT support** changes the app's JIT memory allocator so it can cooperate with a script where TXM/SPTM is present.
|
- **Part 1: Add iOS 26 JIT support** changes the app's JIT memory allocator so it can cooperate with a script where TXM/SPTM is present.
|
||||||
- **Part 2: Integrate StikDebug and StikJIT** adds the StikDebug URL scheme and optional Built-in StikJIT to an app that already works with iOS 26 JIT and manual StikDebug activation.
|
- **Part 2: Integrate StikDebug and StikJIT** adds the StikDebug URL scheme and optional Built-in StikJIT to an app that already works with iOS 26 JIT and manual StikDebug activation.
|
||||||
|
|
||||||
If the app already enables JIT successfully after the user manually selects it and its script in StikDebug, skip Part 1. Record the script filename and complete Part 2. Part 2 does not require changing a working JIT allocator.
|
If the app already enables JIT successfully after the user manually selects it and its script in StikDebug, skip Part 1. Record whether it uses `universal.js`, `legacy.js`, or a custom script file, then complete Part 2. Part 2 does not require changing a working JIT allocator.
|
||||||
|
|
||||||
If the app does not already have script-based iOS 26 JIT, complete Part 1 first and then Part 2.
|
If the app does not already have script-based iOS 26 JIT, complete Part 1 first and then Part 2.
|
||||||
|
|
||||||
|
|
@ -56,11 +56,9 @@ Use this order where TXM/SPTM is present:
|
||||||
|
|
||||||
Preallocate executable regions before detaching. A region introduced later cannot be prepared by a script that has already detached; reconnect and repeat the protocol if the engine must add one.
|
Preallocate executable regions before detaching. A region introduced later cannot be prepared by a script that has already detached; reconnect and repeat the protocol if the engine must add one.
|
||||||
|
|
||||||
## Keep the legacy protocol only for backward compatibility
|
> **Note:** We keep legacy script support only for backward compatibility with existing applications. If you are adding JIT support to an app now, use the universal script.
|
||||||
|
|
||||||
The legacy script is retained only for existing engines that stop once at `brk #0x69` with the RX address in `x0` and its length in `x1`. It prepares that region, advances the program counter, and detaches. Do not build new integrations around `legacy.js`; implement the universal protocol instead.
|
After Part 1 works with the matching script in StikDebug, continue with Part 2. The chosen script is developer-controlled and must not become a user setting.
|
||||||
|
|
||||||
After Part 1 works with the matching script in StikDebug, continue with Part 2. The chosen script filename is developer-controlled and must not become a user setting.
|
|
||||||
|
|
||||||
# Part 2: Integrate StikDebug and StikJIT into an existing iOS 26 JIT app
|
# Part 2: Integrate StikDebug and StikJIT into an existing iOS 26 JIT app
|
||||||
|
|
||||||
|
|
@ -69,7 +67,9 @@ Part 2 assumes all of the following already work:
|
||||||
- The app can wait for an external debugger and detect when JIT is ready.
|
- The app can wait for an external debugger and detect when JIT is ready.
|
||||||
- The app's JIT allocator implements its iOS 26 breakpoint and executable-region protocol.
|
- The app's JIT allocator implements its iOS 26 breakpoint and executable-region protocol.
|
||||||
- JIT works when the developer or user manually selects the app and matching script in StikDebug.
|
- JIT works when the developer or user manually selects the app and matching script in StikDebug.
|
||||||
- The developer knows which script filename the app requires, normally `universal.js`.
|
- The developer knows whether the app requires `universal.js`, `legacy.js`, or a custom script file.
|
||||||
|
|
||||||
|
If the app currently uses `legacy.js`, we recommend switching it to the universal method, but this is not required to complete Part 2.
|
||||||
|
|
||||||
Part 2 preserves that existing **Wait for Debugger** path and adds automatic StikDebug URL launching and optional Built-in StikJIT. It does not change the app's JIT allocator or script protocol. If any prerequisite above is missing, complete Part 1 first.
|
Part 2 preserves that existing **Wait for Debugger** path and adds automatic StikDebug URL launching and optional Built-in StikJIT. It does not change the app's JIT allocator or script protocol. If any prerequisite above is missing, complete Part 1 first.
|
||||||
|
|
||||||
|
|
@ -209,15 +209,19 @@ let paths = DDIPaths.default(
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the same `DDIPaths` for preparation, JIT enablement, and cache reset. Do not validate cached DDI versions or contents yourself; StikJIT reuses readable, nonempty files and lets the image mounter decide whether they work.
|
Use the same `DDIPaths` for preparation, JIT enablement, and cache reset. Before iOS 26.4, StikJIT downloads and mounts the three-file personalized DDI. On iOS 26.4 and newer, it downloads and installs the five-file cryptex DDI, which additionally includes cryptex info and a root hash. Do not validate cached DDI versions or contents yourself; StikJIT selects and replaces the cache format required by the running OS.
|
||||||
|
|
||||||
|
A successful DDI mount persists until the device reboots, so it normally needs to be mounted only once per boot.
|
||||||
|
|
||||||
## Built-in StikJIT: Call the APIs at the right time
|
## Built-in StikJIT: Call the APIs at the right time
|
||||||
|
|
||||||
StikJIT's preparation and enablement APIs are synchronous and blocking. Run them on one dedicated serial background queue in the helper. Do not split their ordered work into concurrent operations or convert it to `Task`/`async` calls.
|
StikJIT's preparation and enablement APIs are synchronous and blocking. Run them on one dedicated serial background queue in the helper. Do not split their ordered work into concurrent operations or convert it to `Task`/`async` calls.
|
||||||
|
|
||||||
|
Pass `configuration:` when the integration needs to override the tunnel endpoint or five-second connection timeout. The default endpoint is `10.7.0.1:49152`.
|
||||||
|
|
||||||
### When settings opens
|
### When settings opens
|
||||||
|
|
||||||
Use `StikJIT.isTXMPresent` to show `Present`, `Not Present`, or `Unknown`. This is informational; script selection still belongs to the developer.
|
Use `StikJIT.isTXMPresent` to show `Present`, `Not Present`, or `Unknown`. This is informational; the app's single script remains developer-defined.
|
||||||
|
|
||||||
### When the user selects Prepare JIT
|
### When the user selects Prepare JIT
|
||||||
|
|
||||||
|
|
@ -258,7 +262,11 @@ try StikJIT.enableJIT(
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `.universal` for new integrations. Use `.legacy` only when preserving an existing `legacy.js` ABI, or `.custom(URL)` for another established protocol. Keep that selection in backend code and use the corresponding filename in the StikDebug URL. Do not expose script selection to the user. The user-facing `forceScript` toggle only bypasses TXM detection and runs the developer-selected script regardless.
|
Configure exactly one script in backend code: `.universal` or `.legacy` for the corresponding bundled script, `.custom(URL)` for a script readable from the helper-extension process, or `.customBase64(String)` for base64-encoded UTF-8 script data held elsewhere. For StikDebug requests, send the built-in filename for universal or legacy, or send the base64-encoded contents of the custom script. Do not expose this configuration to the user. The user-facing `forceScript` toggle only bypasses TXM detection and runs the configured script regardless.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let selectedScript: StikJIT.Script = .customBase64(storedScriptBase64)
|
||||||
|
```
|
||||||
|
|
||||||
### When the user resets the cache
|
### When the user resets the cache
|
||||||
|
|
||||||
|
|
@ -268,7 +276,7 @@ Provide a **Reset Developer Disk Image** action that calls:
|
||||||
try StikJIT.resetCachedDDI(at: paths)
|
try StikJIT.resetCachedDDI(at: paths)
|
||||||
```
|
```
|
||||||
|
|
||||||
This removes only the three cached DDI files. The next preparation downloads them again if mounting is necessary.
|
This removes only the five possible cached DDI files. The next preparation downloads the three-file personalized DDI or five-file cryptex DDI required by the running OS if mounting is necessary.
|
||||||
|
|
||||||
## Configure the JIT methods
|
## Configure the JIT methods
|
||||||
|
|
||||||
|
|
@ -282,7 +290,9 @@ Route all three choices through the same host-side coordinator. Check `get-task-
|
||||||
|
|
||||||
For **Wait for Debugger**, keep the app's existing waiting and readiness behavior. Where TXM/SPTM is present, it must continue through the existing breakpoint protocol before reporting success; an ordinary debugger attachment without the correct script is not sufficient.
|
For **Wait for Debugger**, keep the app's existing waiting and readiness behavior. Where TXM/SPTM is present, it must continue through the existing breakpoint protocol before reporting success; an ordinary debugger attachment without the correct script is not sufficient.
|
||||||
|
|
||||||
For **StikDebug**, construct the URL with `URLComponents` and include the bundle ID, current PID, and the developer-selected script filename:
|
For **StikDebug**, always construct the URL with the bundle ID and current PID. If TXM/SPTM is present, also send the app's JIT script. Each app must use exactly one developer-defined script; never add runtime script selection or expose it as a user setting. When TXM/SPTM is not present, omit the script parameter because debugger attachment alone enables JIT.
|
||||||
|
|
||||||
|
This example is for an app that uses the recommended universal script:
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
private let stikDebugScriptName = "universal.js"
|
private let stikDebugScriptName = "universal.js"
|
||||||
|
|
@ -298,9 +308,14 @@ components.host = "enable-jit"
|
||||||
components.queryItems = [
|
components.queryItems = [
|
||||||
URLQueryItem(name: "bundle-id", value: bundleID),
|
URLQueryItem(name: "bundle-id", value: bundleID),
|
||||||
URLQueryItem(name: "pid", value: String(getpid())),
|
URLQueryItem(name: "pid", value: String(getpid())),
|
||||||
URLQueryItem(name: "script-name", value: stikDebugScriptName),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if isTXMPresent {
|
||||||
|
components.queryItems?.append(
|
||||||
|
URLQueryItem(name: "script-name", value: stikDebugScriptName)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
guard let url = components.url else {
|
guard let url = components.url else {
|
||||||
showJITError("Could not create the StikDebug request.")
|
showJITError("Could not create the StikDebug request.")
|
||||||
return
|
return
|
||||||
|
|
@ -309,7 +324,26 @@ guard let url = components.url else {
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `universal.js` for new integrations. Specify `legacy.js` only for an app that already implements the legacy ABI, or another installed script for an established custom protocol. Keep this choice in developer-controlled backend code and do not expose `script-name` as a user setting. StikDebug uses the PID to target the running process and the bundle ID to identify and return to the app.
|
An existing legacy app can use the same code with its fixed script name set to `legacy.js`. A custom-script app must remove `stikDebugScriptName` and replace the `if isTXMPresent` block with this fixed custom-script implementation:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
if isTXMPresent {
|
||||||
|
do {
|
||||||
|
let scriptData = try Data(contentsOf: stikDebugScriptURL)
|
||||||
|
components.queryItems?.append(
|
||||||
|
URLQueryItem(
|
||||||
|
name: "script-data",
|
||||||
|
value: scriptData.base64EncodedString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
showJITError("Could not read the StikDebug script: \(error.localizedDescription)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`stikDebugScriptURL` must identify the app's single custom script in developer-controlled backend code. Sending it through `script-data` means users do not need to install it in StikDebug. Resolve TXM/SPTM presence before building the request rather than treating an unknown result as absent. StikDebug uses the PID to target the running process and the bundle ID to identify and return to the app.
|
||||||
|
|
||||||
If the app checks whether StikDebug is installed with `canOpenURL`, add this to its `Info.plist`:
|
If the app checks whether StikDebug is installed with `canOpenURL`, add this to its `Info.plist`:
|
||||||
|
|
||||||
|
|
@ -373,19 +407,25 @@ Tell users all of the following before their first Built-in StikJIT attempt:
|
||||||
|
|
||||||
Always surface the actual unreachable, download, mount, pairing, and script errors. Do not reduce every failure to “DDI unmounted.”
|
Always surface the actual unreachable, download, mount, pairing, and script errors. Do not reduce every failure to “DDI unmounted.”
|
||||||
|
|
||||||
|
## Request endorsement
|
||||||
|
|
||||||
|
After completing Part 1, Part 2, or both, please get in touch with us in the [idevice Discord server](https://discord.gg/qtgv6QtYbV) or leave a GitHub issue/pull request, so we can endorse your project and offer support to your users. We review app submissions on a case-by-case basis and generally decline projects without an established user base, low-quality substantially AI-generated projects, and forks of apps that already have our endorsement.
|
||||||
|
|
||||||
# Final checklist
|
# Final checklist
|
||||||
|
|
||||||
## All methods
|
## All methods
|
||||||
|
|
||||||
- [ ] New iOS 26 integrations implement the universal protocol; legacy is used only for backward compatibility.
|
- [ ] New iOS 26 integrations implement the universal protocol.
|
||||||
- [ ] The host checks its own `get-task-allow` before starting JIT acquisition.
|
- [ ] The host checks its own `get-task-allow` before starting JIT acquisition.
|
||||||
- [ ] The selected script matches the app's breakpoint protocol and is not user-configurable.
|
- [ ] The app has exactly one developer-configured script that matches its breakpoint protocol.
|
||||||
- [ ] Where TXM/SPTM is present, executable-region preparation succeeds before the workload starts.
|
- [ ] Where TXM/SPTM is present, executable-region preparation succeeds before the workload starts.
|
||||||
- [ ] Settings offer one mutually exclusive JIT method selection and start only that method.
|
- [ ] Settings offer one mutually exclusive JIT method selection and start only that method.
|
||||||
|
|
||||||
### StikDebug
|
### StikDebug
|
||||||
|
|
||||||
- [ ] The URL includes bundle ID, current PID, and the developer-selected `script-name`.
|
- [ ] The URL always includes bundle ID and current PID.
|
||||||
|
- [ ] Where TXM/SPTM is present, the URL includes `script-name` for universal or legacy, or base64-encoded `script-data` for a custom script.
|
||||||
|
- [ ] Where TXM/SPTM is not present, the URL omits both script parameters.
|
||||||
- [ ] `stikdebug` is in `LSApplicationQueriesSchemes` if the app calls `canOpenURL`.
|
- [ ] `stikdebug` is in `LSApplicationQueriesSchemes` if the app calls `canOpenURL`.
|
||||||
- [ ] The app does not treat successful URL opening as successful JIT acquisition.
|
- [ ] The app does not treat successful URL opening as successful JIT acquisition.
|
||||||
- [ ] Inside LiveContainer, the app tells users to enable **Use LiveContainer's Bundle ID**.
|
- [ ] Inside LiveContainer, the app tells users to enable **Use LiveContainer's Bundle ID**.
|
||||||
|
|
|
||||||
67
README.md
67
README.md
|
|
@ -2,72 +2,11 @@
|
||||||
|
|
||||||
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 its bundled JIT scripts (`universal.js`, `legacy.js`) are bundled inside. The target must have the `get-task-allow` entitlement.
|
StikJIT is self-contained and bundles the idevice FFI and its JIT scripts.
|
||||||
|
|
||||||
## Use
|
## Integration
|
||||||
|
|
||||||
For a complete host-app and helper-extension setup, see the [integration guide](INTEGRATION.md).
|
For iOS 26 JIT support, StikDebug URL integration, Built-in StikJIT setup, API usage, requirements, and recommended app settings, see [Integrating StikJIT](INTEGRATION.md).
|
||||||
|
|
||||||
Because it needs a separate process, you call StikJIT from a small app extension that your app launches and hands the target app's PID to (for example over XPC). StikJIT does not include that extension, its launch, or the pairing file; your app provides those.
|
|
||||||
|
|
||||||
The device needs LocalDevVPN connected, and either Wi-Fi or Airplane Mode enabled, before JIT can be enabled or the DDI can be mounted.
|
|
||||||
|
|
||||||
Add `StikJIT.xcframework` to the extension (Embed & Sign). StikJIT's preparation and JIT APIs are synchronous and blocking, so call them off the main thread.
|
|
||||||
|
|
||||||
Choose the JIT script in your integration code. By default StikJIT runs it when TXM is present and uses attach-only JIT when TXM is absent. `forceScript` bypasses TXM detection and runs the selected script regardless:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import StikJIT
|
|
||||||
|
|
||||||
let paths = DDIPaths.default(in: documentsDirectory)
|
|
||||||
|
|
||||||
try StikJIT.enableJIT(
|
|
||||||
targetPID: hostPID,
|
|
||||||
pairingFile: pairingFileURL,
|
|
||||||
ddiPaths: paths,
|
|
||||||
script: .universal,
|
|
||||||
forceScript: forceJITScript,
|
|
||||||
progress: { print("[StikJIT] \($0)") }
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
The developer-selected script can be `.universal`, `.legacy`, or `.custom(fileURL)`. A custom script must be readable from the extension process. Script selection should not be exposed as a user setting; a user-facing toggle maps only to `forceScript`.
|
|
||||||
|
|
||||||
`enableJIT` prepares the device before attaching. It probes the tunnel endpoint, checks whether the DDI is mounted, downloads missing DDI files, mounts and verifies the DDI, determines whether TXM is present, and then enables JIT. These steps run serially in that order. Pass `configuration:` to override the tunnel endpoint and five-second connection timeout; the endpoint defaults to `10.7.0.1:49152`.
|
|
||||||
|
|
||||||
### Developer Disk Image
|
|
||||||
|
|
||||||
The device must have the personalized Developer Disk Image (DDI) mounted before JIT can be enabled. A mount persists until the device reboots, so it only needs to be redone once per boot. Apps can prepare it from settings before launching an extension:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import StikJIT
|
|
||||||
|
|
||||||
let paths = DDIPaths.default(in: documentsDirectory)
|
|
||||||
|
|
||||||
let readiness = StikJIT.prepareDevice(
|
|
||||||
pairingFile: pairingFileURL,
|
|
||||||
paths: paths
|
|
||||||
) { stage in
|
|
||||||
print("[Preparation] \(stage)")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch readiness {
|
|
||||||
case .ready(let securityState):
|
|
||||||
print("Ready; TXM present: \(String(describing: securityState.isTXMPresent))")
|
|
||||||
case .unreachable(let reason):
|
|
||||||
print("Device unreachable: \(reason)")
|
|
||||||
case .preparationFailed(let reason):
|
|
||||||
print("Preparation failed: \(reason)")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
TXM presence is also available without preparing the device through `StikJIT.isTXMPresent`. It returns `true` when TXM is present, `false` when it is absent, and `nil` when detection is unavailable.
|
|
||||||
|
|
||||||
StikJIT treats readable, nonempty DDI files as cached and lets the device image mounter decide whether they are usable. A settings recovery action labeled **Reset Developer Disk Image** can remove the three cached files so the next preparation that needs to mount the DDI downloads them again:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
try StikJIT.resetCachedDDI(at: paths)
|
|
||||||
```
|
|
||||||
|
|
||||||
[](https://deepwiki.com/StephenDev0/StikJIT)
|
[](https://deepwiki.com/StephenDev0/StikJIT)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import Foundation
|
||||||
enum BundledScript {
|
enum BundledScript {
|
||||||
|
|
||||||
static func source(for script: StikJIT.Script) throws -> String {
|
static func source(for script: StikJIT.Script) throws -> String {
|
||||||
if case .custom(let url) = script {
|
switch script {
|
||||||
|
case .custom(let url):
|
||||||
guard url.isFileURL else {
|
guard url.isFileURL else {
|
||||||
throw StikJITError.customScript("expected a file URL")
|
throw StikJITError.customScript("expected a file URL")
|
||||||
}
|
}
|
||||||
|
|
@ -18,6 +19,19 @@ enum BundledScript {
|
||||||
} catch {
|
} catch {
|
||||||
throw StikJITError.customScript("could not read \(url.path): \(error.localizedDescription)")
|
throw StikJITError.customScript("could not read \(url.path): \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
case .customBase64(let encodedSource):
|
||||||
|
guard let data = Data(base64Encoded: encodedSource) else {
|
||||||
|
throw StikJITError.customScript("the provided script data is not valid base64")
|
||||||
|
}
|
||||||
|
guard let source = String(data: data, encoding: .utf8) else {
|
||||||
|
throw StikJITError.customScript("the decoded script data is not valid UTF-8")
|
||||||
|
}
|
||||||
|
guard !source.isEmpty else {
|
||||||
|
throw StikJITError.customScript("the decoded script is empty")
|
||||||
|
}
|
||||||
|
return source
|
||||||
|
case .universal, .legacy:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
let bundle = Bundle(for: BundleToken.self)
|
let bundle = Bundle(for: BundleToken.self)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,29 @@ final class DDISession {
|
||||||
let tunnel = try makeTunnel()
|
let tunnel = try makeTunnel()
|
||||||
defer { tunnel.free() }
|
defer { tunnel.free() }
|
||||||
|
|
||||||
|
if DDIMountMethod.current == .personalized {
|
||||||
|
return try isLegacyDDIMounted(over: tunnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
var installed: UnsafeMutablePointer<InstalledCryptexC>?
|
||||||
|
let cryptexError = cryptexd_installed_ddi(tunnel.adapter, tunnel.handshake, &installed)
|
||||||
|
defer { cryptexd_free_installed_cryptex(installed) }
|
||||||
|
if installed != nil {
|
||||||
|
if let cryptexError { idevice_error_free(cryptexError) }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (try? isLegacyDDIMounted(over: tunnel)) == true {
|
||||||
|
if let cryptexError { idevice_error_free(cryptexError) }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if let cryptexError {
|
||||||
|
try IdeviceFFI.check("failed to query installed DDI cryptex") { cryptexError }
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isLegacyDDIMounted(over tunnel: Tunnel) throws -> Bool {
|
||||||
var client: OpaquePointer?
|
var client: OpaquePointer?
|
||||||
try IdeviceFFI.check("failed to connect to image mounter") {
|
try IdeviceFFI.check("failed to connect to image mounter") {
|
||||||
image_mounter_connect_rsd(tunnel.adapter, tunnel.handshake, &client)
|
image_mounter_connect_rsd(tunnel.adapter, tunnel.handshake, &client)
|
||||||
|
|
@ -57,10 +80,39 @@ final class DDISession {
|
||||||
return deviceCount > 0
|
return deviceCount > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func mountDDI(imagePath: String, trustcachePath: String, manifestPath: String, progress: @escaping (Double) -> Void) throws {
|
func mountDDI(paths: DDIPaths, progress: @escaping (Double) -> Void) throws {
|
||||||
let imageData = try mappedFileData(atPath: imagePath, description: "developer disk image")
|
switch DDIMountMethod.current {
|
||||||
let trustcacheData = try mappedFileData(atPath: trustcachePath, description: "developer disk image trust cache")
|
case .cryptex:
|
||||||
let manifestData = try mappedFileData(atPath: manifestPath, description: "developer disk image manifest")
|
try installCryptexDDI(from: URL(fileURLWithPath: paths.manifestPath).deletingLastPathComponent().path, progress: progress)
|
||||||
|
case .personalized:
|
||||||
|
try mountPersonalizedDDI(paths: paths, progress: progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func installCryptexDDI(from directoryPath: String, progress: @escaping (Double) -> Void) throws {
|
||||||
|
progress(0)
|
||||||
|
var assets: OpaquePointer?
|
||||||
|
try IdeviceFFI.check("failed to load DDI cryptex assets") {
|
||||||
|
directoryPath.withCString { cryptex1_assets_load($0, &assets) }
|
||||||
|
}
|
||||||
|
guard let assets else {
|
||||||
|
throw StikJITError.ddiFilesMissing("DDI cryptex assets could not be loaded from \(directoryPath)")
|
||||||
|
}
|
||||||
|
defer { cryptex1_assets_free(assets) }
|
||||||
|
|
||||||
|
let tunnel = try makeTunnel()
|
||||||
|
defer { tunnel.free() }
|
||||||
|
|
||||||
|
try IdeviceFFI.check("failed to install DDI cryptex") {
|
||||||
|
cryptexd_install_ddi(tunnel.adapter, tunnel.handshake, assets, nil)
|
||||||
|
}
|
||||||
|
progress(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mountPersonalizedDDI(paths: DDIPaths, progress: @escaping (Double) -> Void) throws {
|
||||||
|
let imageData = try mappedFileData(atPath: paths.imagePath, description: "developer disk image")
|
||||||
|
let trustcacheData = try mappedFileData(atPath: paths.trustcachePath, description: "developer disk image trust cache")
|
||||||
|
let manifestData = try mappedFileData(atPath: paths.manifestPath, description: "developer disk image manifest")
|
||||||
|
|
||||||
let tunnel = try makeTunnel()
|
let tunnel = try makeTunnel()
|
||||||
defer { tunnel.free() }
|
defer { tunnel.free() }
|
||||||
|
|
@ -78,9 +130,9 @@ final class DDISession {
|
||||||
let context = Unmanaged.passUnretained(progressBox).toOpaque()
|
let context = Unmanaged.passUnretained(progressBox).toOpaque()
|
||||||
|
|
||||||
try IdeviceFFI.check("failed to mount personalized developer disk image") {
|
try IdeviceFFI.check("failed to mount personalized developer disk image") {
|
||||||
imageData.withUnsafeBytes { imageBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
|
imageData.withUnsafeBytes { imageBuffer in
|
||||||
trustcacheData.withUnsafeBytes { trustcacheBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
|
trustcacheData.withUnsafeBytes { trustcacheBuffer in
|
||||||
manifestData.withUnsafeBytes { manifestBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
|
manifestData.withUnsafeBytes { manifestBuffer in
|
||||||
image_mounter_mount_personalized_with_callback_rsd(
|
image_mounter_mount_personalized_with_callback_rsd(
|
||||||
client,
|
client,
|
||||||
tunnel.adapter,
|
tunnel.adapter,
|
||||||
|
|
@ -131,6 +183,9 @@ final class DDISession {
|
||||||
|
|
||||||
var value: UInt64 = 0
|
var value: UInt64 = 0
|
||||||
plist_get_uint_val(uniqueChipIDPlist, &value)
|
plist_get_uint_val(uniqueChipIDPlist, &value)
|
||||||
|
guard value != 0 else {
|
||||||
|
throw StikJITError.device(code: -1, subCode: 0, message: "Failed to decode UniqueChipID")
|
||||||
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,20 @@ public struct DDIPaths: Sendable {
|
||||||
|
|
||||||
public var manifestPath: String
|
public var manifestPath: String
|
||||||
|
|
||||||
public init(imagePath: String, trustcachePath: String, manifestPath: String) {
|
public var cryptexInfoPath: String
|
||||||
|
|
||||||
|
public var rootHashPath: String
|
||||||
|
|
||||||
|
public init(imagePath: String,
|
||||||
|
trustcachePath: String,
|
||||||
|
manifestPath: String,
|
||||||
|
cryptexInfoPath: String? = nil,
|
||||||
|
rootHashPath: String? = nil) {
|
||||||
self.imagePath = imagePath
|
self.imagePath = imagePath
|
||||||
self.trustcachePath = trustcachePath
|
self.trustcachePath = trustcachePath
|
||||||
self.manifestPath = manifestPath
|
self.manifestPath = manifestPath
|
||||||
|
self.cryptexInfoPath = cryptexInfoPath ?? "\(imagePath).cryptex_info"
|
||||||
|
self.rootHashPath = rootHashPath ?? "\(imagePath).root_hash"
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func `default`(in directory: URL) -> DDIPaths {
|
public static func `default`(in directory: URL) -> DDIPaths {
|
||||||
|
|
@ -23,7 +33,8 @@ public struct DDIPaths: Sendable {
|
||||||
|
|
||||||
var allFilesUsable: Bool {
|
var allFilesUsable: Bool {
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
return allPaths.allSatisfy { path in
|
let requiredPaths = DDIMountMethod.current == .cryptex ? allPaths : personalizedPaths
|
||||||
|
let requiredFilesUsable = requiredPaths.allSatisfy { path in
|
||||||
guard fileManager.isReadableFile(atPath: path),
|
guard fileManager.isReadableFile(atPath: path),
|
||||||
let attributes = try? fileManager.attributesOfItem(atPath: path),
|
let attributes = try? fileManager.attributesOfItem(atPath: path),
|
||||||
attributes[.type] as? FileAttributeType == .typeRegular,
|
attributes[.type] as? FileAttributeType == .typeRegular,
|
||||||
|
|
@ -32,18 +43,49 @@ public struct DDIPaths: Sendable {
|
||||||
}
|
}
|
||||||
return size.int64Value > 0
|
return size.int64Value > 0
|
||||||
}
|
}
|
||||||
|
if DDIMountMethod.current == .personalized {
|
||||||
|
return requiredFilesUsable && cryptexOnlyPaths.allSatisfy { !fileManager.fileExists(atPath: $0) }
|
||||||
|
}
|
||||||
|
return requiredFilesUsable
|
||||||
}
|
}
|
||||||
|
|
||||||
var allPaths: [String] {
|
var allPaths: [String] {
|
||||||
|
personalizedPaths + cryptexOnlyPaths
|
||||||
|
}
|
||||||
|
|
||||||
|
var personalizedPaths: [String] {
|
||||||
[imagePath, trustcachePath, manifestPath]
|
[imagePath, trustcachePath, manifestPath]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cryptexOnlyPaths: [String] {
|
||||||
|
[cryptexInfoPath, rootHashPath]
|
||||||
|
}
|
||||||
|
|
||||||
func removeCachedFiles() throws {
|
func removeCachedFiles() throws {
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
for path in Set(allPaths) where fileManager.fileExists(atPath: path) {
|
for path in Set(allPaths) where fileManager.fileExists(atPath: path) {
|
||||||
try fileManager.removeItem(atPath: path)
|
try fileManager.removeItem(atPath: path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func removeCryptexOnlyFiles() throws {
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
for path in Set(cryptexOnlyPaths) where fileManager.fileExists(atPath: path) {
|
||||||
|
try fileManager.removeItem(atPath: path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DDIMountMethod: String {
|
||||||
|
case cryptex
|
||||||
|
case personalized
|
||||||
|
|
||||||
|
static var current: DDIMountMethod {
|
||||||
|
let version = ProcessInfo.processInfo.operatingSystemVersion
|
||||||
|
return version.majorVersion > 26 || (version.majorVersion == 26 && version.minorVersion >= 4)
|
||||||
|
? .cryptex
|
||||||
|
: .personalized
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DDIDownloadItem {
|
struct DDIDownloadItem {
|
||||||
|
|
@ -53,14 +95,19 @@ struct DDIDownloadItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DDIDownloadCatalog {
|
enum DDIDownloadCatalog {
|
||||||
private static let baseURL = URL(string: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized")!
|
|
||||||
|
|
||||||
static func items(for paths: DDIPaths) -> [DDIDownloadItem] {
|
static func items(for paths: DDIPaths) -> [DDIDownloadItem] {
|
||||||
[
|
let directory = DDIMountMethod.current == .cryptex ? "Xcode_iOS_DDI_Cryptex" : "Xcode_iOS_DDI_Personalized"
|
||||||
|
let baseURL = URL(string: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/\(directory)")!
|
||||||
|
var items = [
|
||||||
DDIDownloadItem(name: "BuildManifest.plist", destinationPath: paths.manifestPath, url: baseURL.appendingPathComponent("BuildManifest.plist")),
|
DDIDownloadItem(name: "BuildManifest.plist", destinationPath: paths.manifestPath, url: baseURL.appendingPathComponent("BuildManifest.plist")),
|
||||||
DDIDownloadItem(name: "Image.dmg", destinationPath: paths.imagePath, url: baseURL.appendingPathComponent("Image.dmg")),
|
DDIDownloadItem(name: "Image.dmg", destinationPath: paths.imagePath, url: baseURL.appendingPathComponent("Image.dmg")),
|
||||||
DDIDownloadItem(name: "Image.dmg.trustcache", destinationPath: paths.trustcachePath, url: baseURL.appendingPathComponent("Image.dmg.trustcache")),
|
DDIDownloadItem(name: "Image.dmg.trustcache", destinationPath: paths.trustcachePath, url: baseURL.appendingPathComponent("Image.dmg.trustcache")),
|
||||||
]
|
]
|
||||||
|
if DDIMountMethod.current == .cryptex {
|
||||||
|
items.append(DDIDownloadItem(name: "Image.dmg.cryptex_info", destinationPath: paths.cryptexInfoPath, url: baseURL.appendingPathComponent("Image.dmg.cryptex_info")))
|
||||||
|
items.append(DDIDownloadItem(name: "Image.dmg.root_hash", destinationPath: paths.rootHashPath, url: baseURL.appendingPathComponent("Image.dmg.root_hash")))
|
||||||
|
}
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,6 +138,9 @@ public actor DeveloperDiskImageService {
|
||||||
try await downloadFile(from: item.url, to: URL(fileURLWithPath: item.destinationPath))
|
try await downloadFile(from: item.url, to: URL(fileURLWithPath: item.destinationPath))
|
||||||
progress(Double(index + 1) / total, "\(item.name) ready")
|
progress(Double(index + 1) / total, "\(item.name) ready")
|
||||||
}
|
}
|
||||||
|
if DDIMountMethod.current == .personalized {
|
||||||
|
try paths.removeCryptexOnlyFiles()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func downloadFile(from url: URL, to destinationURL: URL) async throws {
|
private func downloadFile(from url: URL, to destinationURL: URL) async throws {
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,14 @@ public enum StikJIT {
|
||||||
|
|
||||||
case custom(URL)
|
case custom(URL)
|
||||||
|
|
||||||
|
case customBase64(String)
|
||||||
|
|
||||||
var name: String {
|
var name: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .universal: return "universal"
|
case .universal: return "universal"
|
||||||
case .legacy: return "legacy"
|
case .legacy: return "legacy"
|
||||||
case .custom(let url): return url.lastPathComponent
|
case .custom(let url): return url.lastPathComponent
|
||||||
|
case .customBase64: return "custom"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -144,9 +147,7 @@ public enum StikJIT {
|
||||||
progress(.downloadingDDI(fraction: fraction, status: status))
|
progress(.downloadingDDI(fraction: fraction, status: status))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try session.mountDDI(imagePath: paths.imagePath,
|
try session.mountDDI(paths: paths) { fraction in
|
||||||
trustcachePath: paths.trustcachePath,
|
|
||||||
manifestPath: paths.manifestPath) { fraction in
|
|
||||||
progress(.mountingDDI(fraction: fraction))
|
progress(.mountingDDI(fraction: fraction))
|
||||||
}
|
}
|
||||||
progress(.verifyingDDI)
|
progress(.verifyingDDI)
|
||||||
|
|
@ -183,9 +184,6 @@ public enum StikJIT {
|
||||||
paths: DDIPaths,
|
paths: DDIPaths,
|
||||||
progress: @escaping (Double) -> Void = { _ in }) throws {
|
progress: @escaping (Double) -> Void = { _ in }) throws {
|
||||||
let session = DDISession(pairingFilePath: pairingFile.path, configuration: configuration)
|
let session = DDISession(pairingFilePath: pairingFile.path, configuration: configuration)
|
||||||
try session.mountDDI(imagePath: paths.imagePath,
|
try session.mountDDI(paths: paths, progress: progress)
|
||||||
trustcachePath: paths.trustcachePath,
|
|
||||||
manifestPath: paths.manifestPath,
|
|
||||||
progress: progress)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ enum SynchronousDDIDownloader {
|
||||||
try download(item, session: session)
|
try download(item, session: session)
|
||||||
progress(Double(index + 1) / total, "\(item.name) ready")
|
progress(Double(index + 1) / total, "\(item.name) ready")
|
||||||
}
|
}
|
||||||
|
if DDIMountMethod.current == .personalized {
|
||||||
|
try paths.removeCryptexOnlyFiles()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func download(_ item: DDIDownloadItem, session: URLSession) throws {
|
private static func download(_ item: DDIDownloadItem, session: URLSession) throws {
|
||||||
|
|
|
||||||
3733
idevice/idevice.h
3733
idevice/idevice.h
File diff suppressed because it is too large
Load diff
Binary file not shown.
16
project.yml
16
project.yml
|
|
@ -8,8 +8,8 @@ options:
|
||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
SWIFT_VERSION: "5.0"
|
SWIFT_VERSION: "5.0"
|
||||||
MARKETING_VERSION: "1.5.0"
|
MARKETING_VERSION: "1.9.0"
|
||||||
CURRENT_PROJECT_VERSION: "6"
|
CURRENT_PROJECT_VERSION: "10"
|
||||||
CLANG_CXX_LIBRARY: "libc++"
|
CLANG_CXX_LIBRARY: "libc++"
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
|
|
@ -34,12 +34,18 @@ targets:
|
||||||
DEFINES_MODULE: YES
|
DEFINES_MODULE: YES
|
||||||
BUILD_LIBRARY_FOR_DISTRIBUTION: YES
|
BUILD_LIBRARY_FOR_DISTRIBUTION: YES
|
||||||
SKIP_INSTALL: NO
|
SKIP_INSTALL: NO
|
||||||
# idevice FFI: modulemap/headers to compile `import idevice`, and force_load the
|
# Embed only the idevice code reachable from StikJIT. Keep archive symbols
|
||||||
# static lib so all its symbols are embedded in the framework binary (self-contained).
|
# private so unused FFI/Rust exports do not become dead-strip roots.
|
||||||
SWIFT_INCLUDE_PATHS: "$(SRCROOT)/idevice $(SRCROOT)/iokit"
|
SWIFT_INCLUDE_PATHS: "$(SRCROOT)/idevice $(SRCROOT)/iokit"
|
||||||
HEADER_SEARCH_PATHS: "$(SRCROOT)/idevice $(SRCROOT)/iokit"
|
HEADER_SEARCH_PATHS: "$(SRCROOT)/idevice $(SRCROOT)/iokit"
|
||||||
LIBRARY_SEARCH_PATHS: "$(SRCROOT)/idevice"
|
LIBRARY_SEARCH_PATHS: "$(SRCROOT)/idevice"
|
||||||
OTHER_LDFLAGS: "-force_load $(SRCROOT)/idevice/libidevice_ffi.a"
|
OTHER_LDFLAGS: "$(inherited) -Wl,-hidden-lidevice_ffi"
|
||||||
|
DEAD_CODE_STRIPPING: YES
|
||||||
|
configs:
|
||||||
|
Release:
|
||||||
|
# Remove private symbol names from the shipped binary; keep the dSYM.
|
||||||
|
STRIP_INSTALLED_PRODUCT: YES
|
||||||
|
STRIP_STYLE: non-global
|
||||||
|
|
||||||
schemes:
|
schemes:
|
||||||
StikJIT:
|
StikJIT:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue