mirror of
https://github.com/StikDebug/StikJIT.git
synced 2026-10-15 20:10:31 +00:00
fix: restore personalized DDI mounting
This commit is contained in:
parent
5d732f94b8
commit
32287268fa
6 changed files with 160 additions and 15 deletions
|
|
@ -209,7 +209,7 @@ let paths = DDIPaths.default(
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the same `DDIPaths` for preparation, JIT enablement, and cache reset. The cache contains the image, trust cache, build manifest, cryptex info, and root hash. Do not validate cached DDI versions or contents yourself; StikJIT reuses readable, nonempty files and lets the device's cryptex service 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.
|
A successful DDI mount persists until the device reboots, so it normally needs to be mounted only once per boot.
|
||||||
|
|
||||||
|
|
@ -276,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 five 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
@_implementationOnly import idevice
|
@_implementationOnly import idevice
|
||||||
|
|
||||||
|
private func ddiMountProgressCallback(progress: size_t, total: size_t, context: UnsafeMutableRawPointer?) {
|
||||||
|
guard let context else { return }
|
||||||
|
let progressHandler = Unmanaged<DDIProgressBox>.fromOpaque(context).takeUnretainedValue()
|
||||||
|
let fraction = total > 0 ? Double(progress) / Double(total) : 0
|
||||||
|
progressHandler.handler(fraction)
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class DDIProgressBox {
|
||||||
|
let handler: (Double) -> Void
|
||||||
|
init(_ handler: @escaping (Double) -> Void) { self.handler = handler }
|
||||||
|
}
|
||||||
|
|
||||||
final class DDISession {
|
final class DDISession {
|
||||||
|
|
||||||
private struct Tunnel {
|
private struct Tunnel {
|
||||||
|
|
@ -24,6 +36,10 @@ 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>?
|
var installed: UnsafeMutablePointer<InstalledCryptexC>?
|
||||||
let cryptexError = cryptexd_installed_ddi(tunnel.adapter, tunnel.handshake, &installed)
|
let cryptexError = cryptexd_installed_ddi(tunnel.adapter, tunnel.handshake, &installed)
|
||||||
defer { cryptexd_free_installed_cryptex(installed) }
|
defer { cryptexd_free_installed_cryptex(installed) }
|
||||||
|
|
@ -64,7 +80,16 @@ final class DDISession {
|
||||||
return deviceCount > 0
|
return deviceCount > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func mountDDI(from directoryPath: String, progress: @escaping (Double) -> Void) throws {
|
func mountDDI(paths: DDIPaths, progress: @escaping (Double) -> Void) throws {
|
||||||
|
switch DDIMountMethod.current {
|
||||||
|
case .cryptex:
|
||||||
|
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)
|
progress(0)
|
||||||
var assets: OpaquePointer?
|
var assets: OpaquePointer?
|
||||||
try IdeviceFFI.check("failed to load DDI cryptex assets") {
|
try IdeviceFFI.check("failed to load DDI cryptex assets") {
|
||||||
|
|
@ -84,6 +109,86 @@ final class DDISession {
|
||||||
progress(1)
|
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()
|
||||||
|
defer { tunnel.free() }
|
||||||
|
|
||||||
|
let uniqueChipID = try fetchUniqueChipID(over: tunnel)
|
||||||
|
|
||||||
|
var client: OpaquePointer?
|
||||||
|
try IdeviceFFI.check("failed to connect to image mounter") {
|
||||||
|
image_mounter_connect_rsd(tunnel.adapter, tunnel.handshake, &client)
|
||||||
|
}
|
||||||
|
guard let client else { throw StikJITError.debugProxyUnavailable }
|
||||||
|
defer { image_mounter_free(client) }
|
||||||
|
|
||||||
|
let progressBox = DDIProgressBox(progress)
|
||||||
|
let context = Unmanaged.passUnretained(progressBox).toOpaque()
|
||||||
|
|
||||||
|
try IdeviceFFI.check("failed to mount personalized developer disk image") {
|
||||||
|
imageData.withUnsafeBytes { imageBuffer in
|
||||||
|
trustcacheData.withUnsafeBytes { trustcacheBuffer in
|
||||||
|
manifestData.withUnsafeBytes { manifestBuffer in
|
||||||
|
image_mounter_mount_personalized_with_callback_rsd(
|
||||||
|
client,
|
||||||
|
tunnel.adapter,
|
||||||
|
tunnel.handshake,
|
||||||
|
imageBuffer.bindMemory(to: UInt8.self).baseAddress,
|
||||||
|
imageData.count,
|
||||||
|
trustcacheBuffer.bindMemory(to: UInt8.self).baseAddress,
|
||||||
|
trustcacheData.count,
|
||||||
|
manifestBuffer.bindMemory(to: UInt8.self).baseAddress,
|
||||||
|
manifestData.count,
|
||||||
|
nil,
|
||||||
|
uniqueChipID,
|
||||||
|
ddiMountProgressCallback,
|
||||||
|
context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mappedFileData(atPath path: String, description: String) throws -> Data {
|
||||||
|
guard FileManager.default.fileExists(atPath: path) else {
|
||||||
|
throw StikJITError.ddiFilesMissing("\(description) not found at \(path)")
|
||||||
|
}
|
||||||
|
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
|
||||||
|
guard !data.isEmpty else {
|
||||||
|
throw StikJITError.ddiFilesMissing("\(description) is empty at \(path)")
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchUniqueChipID(over tunnel: Tunnel) throws -> UInt64 {
|
||||||
|
var client: OpaquePointer?
|
||||||
|
try IdeviceFFI.check("failed to connect to lockdownd") {
|
||||||
|
lockdownd_connect_rsd(tunnel.adapter, tunnel.handshake, &client)
|
||||||
|
}
|
||||||
|
guard let client else { throw StikJITError.debugProxyUnavailable }
|
||||||
|
defer { lockdownd_client_free(client) }
|
||||||
|
|
||||||
|
var uniqueChipIDPlist: plist_t?
|
||||||
|
try IdeviceFFI.check("failed to query UniqueChipID") {
|
||||||
|
"UniqueChipID".withCString { lockdownd_get_value(client, $0, nil, &uniqueChipIDPlist) }
|
||||||
|
}
|
||||||
|
guard let uniqueChipIDPlist else {
|
||||||
|
throw StikJITError.device(code: -1, subCode: 0, message: "UniqueChipID was not returned by lockdownd")
|
||||||
|
}
|
||||||
|
defer { plist_free(uniqueChipIDPlist) }
|
||||||
|
|
||||||
|
var value: UInt64 = 0
|
||||||
|
plist_get_uint_val(uniqueChipIDPlist, &value)
|
||||||
|
guard value != 0 else {
|
||||||
|
throw StikJITError.device(code: -1, subCode: 0, message: "Failed to decode UniqueChipID")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
private func openPairingFile() throws -> OpaquePointer {
|
private func openPairingFile() throws -> OpaquePointer {
|
||||||
guard !pairingFilePath.isEmpty, FileManager.default.fileExists(atPath: pairingFilePath) else {
|
guard !pairingFilePath.isEmpty, FileManager.default.fileExists(atPath: pairingFilePath) else {
|
||||||
throw StikJITError.pairingFile("not found at \(pairingFilePath)")
|
throw StikJITError.pairingFile("not found at \(pairingFilePath)")
|
||||||
|
|
|
||||||
|
|
@ -33,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,
|
||||||
|
|
@ -42,10 +43,22 @@ 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] {
|
||||||
[imagePath, trustcachePath, manifestPath, cryptexInfoPath, rootHashPath]
|
personalizedPaths + cryptexOnlyPaths
|
||||||
|
}
|
||||||
|
|
||||||
|
var personalizedPaths: [String] {
|
||||||
|
[imagePath, trustcachePath, manifestPath]
|
||||||
|
}
|
||||||
|
|
||||||
|
var cryptexOnlyPaths: [String] {
|
||||||
|
[cryptexInfoPath, rootHashPath]
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeCachedFiles() throws {
|
func removeCachedFiles() throws {
|
||||||
|
|
@ -54,6 +67,25 @@ public struct DDIPaths: Sendable {
|
||||||
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 {
|
||||||
|
|
@ -63,16 +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_Cryptex")!
|
|
||||||
|
|
||||||
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")),
|
||||||
DDIDownloadItem(name: "Image.dmg.cryptex_info", destinationPath: paths.cryptexInfoPath, url: baseURL.appendingPathComponent("Image.dmg.cryptex_info")),
|
|
||||||
DDIDownloadItem(name: "Image.dmg.root_hash", destinationPath: paths.rootHashPath, url: baseURL.appendingPathComponent("Image.dmg.root_hash")),
|
|
||||||
]
|
]
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,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 {
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ public enum StikJIT {
|
||||||
progress(.downloadingDDI(fraction: fraction, status: status))
|
progress(.downloadingDDI(fraction: fraction, status: status))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try session.mountDDI(from: URL(fileURLWithPath: paths.manifestPath).deletingLastPathComponent().path) { fraction in
|
try session.mountDDI(paths: paths) { fraction in
|
||||||
progress(.mountingDDI(fraction: fraction))
|
progress(.mountingDDI(fraction: fraction))
|
||||||
}
|
}
|
||||||
progress(.verifyingDDI)
|
progress(.verifyingDDI)
|
||||||
|
|
@ -184,7 +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(from: URL(fileURLWithPath: paths.manifestPath).deletingLastPathComponent().path,
|
try session.mountDDI(paths: paths, progress: progress)
|
||||||
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 {
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ options:
|
||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
SWIFT_VERSION: "5.0"
|
SWIFT_VERSION: "5.0"
|
||||||
MARKETING_VERSION: "1.8.0"
|
MARKETING_VERSION: "1.9.0"
|
||||||
CURRENT_PROJECT_VERSION: "9"
|
CURRENT_PROJECT_VERSION: "10"
|
||||||
CLANG_CXX_LIBRARY: "libc++"
|
CLANG_CXX_LIBRARY: "libc++"
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue