fix: restore personalized DDI mounting

This commit is contained in:
CelloSerenity 2026-09-27 13:16:09 -06:00
parent 534b6daa3f
commit cbaf467b66
4 changed files with 164 additions and 10 deletions

View file

@ -44,6 +44,35 @@ private enum IdeviceBridge {
return makeError(domain: domain, code: code, message: message)
}
static func mappedFileData(atPath path: String, description: String) throws -> Data {
let url = URL(fileURLWithPath: path)
do {
let data = try Data(contentsOf: url, options: .mappedIfSafe)
guard !data.isEmpty else {
throw makeError(message: "\(description) is empty")
}
return data
} catch let error as NSError {
throw makeError(code: error.code, message: "Failed to read \(description): \(error.localizedDescription)")
}
}
static func uint64Value(from plist: plist_t?, fieldName: String) throws -> UInt64 {
guard let plist else {
throw makeError(message: "\(fieldName) was not returned by lockdownd")
}
var value: UInt64 = 0
plist_get_uint_val(plist, &value)
guard value != 0 else {
throw makeError(message: "Failed to decode \(fieldName)")
}
return value
}
static func withTunnelHandles<T>(
for context: JITEnableContext,
_ body: (OpaquePointer, OpaquePointer) throws -> T
@ -295,6 +324,59 @@ extension JITEnableContext {
}
}
func mountPersonalDDI(withImagePath imagePath: String, trustcachePath: String, manifestPath: String) throws {
let imageData = try IdeviceBridge.mappedFileData(atPath: imagePath, description: "developer disk image")
let trustcacheData = try IdeviceBridge.mappedFileData(atPath: trustcachePath, description: "developer disk image trust cache")
let manifestData = try IdeviceBridge.mappedFileData(atPath: manifestPath, description: "developer disk image manifest")
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
let uniqueChipID = try IdeviceBridge.withConnectedClient(
fallback: "Failed to connect to lockdownd",
missingClientMessage: "Lockdownd client was not created",
connect: { lockdownd_connect_rsd(adapter, handshake, $0) },
cleanup: { lockdownd_client_free($0) }
) { lockdownClient in
var uniqueChipIDPlist: plist_t?
if let ffiError = lockdownd_get_value(lockdownClient, "UniqueChipID", nil, &uniqueChipIDPlist) {
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to query UniqueChipID")
}
defer { plist_free(uniqueChipIDPlist) }
return try IdeviceBridge.uint64Value(from: uniqueChipIDPlist, fieldName: "UniqueChipID")
}
try IdeviceBridge.withConnectedClient(
fallback: "Failed to connect to image mounter",
missingClientMessage: "Image mounter client was not created",
connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
cleanup: { image_mounter_free($0) }
) { imageMounterClient in
let ffiError = imageData.withUnsafeBytes { imageBuffer in
trustcacheData.withUnsafeBytes { trustcacheBuffer in
manifestData.withUnsafeBytes { manifestBuffer in
image_mounter_mount_personalized_rsd(
imageMounterClient,
adapter,
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
)
}
}
}
if let ffiError {
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to mount personalized DDI")
}
}
}
}
func fetchAllProfiles() throws -> [Data] {
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
try IdeviceBridge.withConnectedClient(

View file

@ -93,7 +93,7 @@ final class MountingProgress: ObservableObject {
self.coolisMounted = true
self.checkforMounted()
} else if let mountError {
LogManager.shared.addErrorLog("Failed to install DDI cryptex: \(mountError)")
LogManager.shared.addErrorLog("Failed to mount DDI: \(mountError)")
showAlert(title: "DDI Mount Failed", message: mountError, showOk: true, showTryAgain: true) { shouldTryAgain in
if shouldTryAgain {
self.pubMount()

View file

@ -33,7 +33,15 @@ func checkMountStatus() -> MountCheckResult {
func mountDeveloperDiskImage(from directoryPath: String) -> String? {
do {
try JITEnableContext.shared.installCryptexDDI(from: directoryPath)
if DeveloperDiskImageService.usesCryptexDDI {
try JITEnableContext.shared.installCryptexDDI(from: directoryPath)
} else {
try JITEnableContext.shared.mountPersonalDDI(
withImagePath: URL(fileURLWithPath: directoryPath).appendingPathComponent("Image.dmg").path,
trustcachePath: URL(fileURLWithPath: directoryPath).appendingPathComponent("Image.dmg.trustcache").path,
manifestPath: URL(fileURLWithPath: directoryPath).appendingPathComponent("BuildManifest.plist").path
)
}
} catch {
return error.localizedDescription
}

View file

@ -12,7 +12,11 @@ final class DeveloperDiskImageService {
static var filesAreReady: Bool {
downloadItems.allSatisfy {
FileManager.default.fileExists(atPath: directoryURL.appendingPathComponent($0.fileName).path)
}
} && (try? String(contentsOf: mountMethodURL, encoding: .utf8)) == mountMethod.rawValue
}
static var usesCryptexDDI: Bool {
mountMethod == .cryptex
}
private let fileManager: FileManager
@ -24,20 +28,21 @@ final class DeveloperDiskImageService {
}
func downloadMissingFiles() async throws {
let hasCryptexMarker = ["Image.dmg.cryptex_info", "Image.dmg.root_hash"].contains {
fileManager.fileExists(atPath: Self.directoryURL.appendingPathComponent($0).path)
}
let replaceLegacyFiles = !hasCryptexMarker && Self.downloadItems.contains {
fileManager.fileExists(atPath: Self.directoryURL.appendingPathComponent($0.fileName).path)
let installedMethod = try? String(contentsOf: Self.mountMethodURL, encoding: .utf8)
let replaceFiles = installedMethod != Self.mountMethod.rawValue
if !Self.usesCryptexDDI {
try removeCryptexOnlyFiles()
}
for item in Self.downloadItems {
let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName)
guard replaceLegacyFiles || !fileManager.fileExists(atPath: destinationURL.path) else {
guard replaceFiles || !fileManager.fileExists(atPath: destinationURL.path) else {
continue
}
try await downloadFile(from: item.urlString, to: destinationURL)
}
try Self.mountMethod.rawValue.write(to: Self.mountMethodURL, atomically: true, encoding: .utf8)
}
func downloadFile(from urlString: String, to destinationURL: URL) async throws {
@ -70,6 +75,12 @@ final class DeveloperDiskImageService {
var completedStages = 0.0
progressHandler?(0.0, "Removing existing DDI files...")
if fileManager.fileExists(atPath: Self.mountMethodURL.path) {
try fileManager.removeItem(at: Self.mountMethodURL)
}
if !Self.usesCryptexDDI {
try removeCryptexOnlyFiles()
}
let completionMarker = Self.directoryURL.appendingPathComponent("Image.dmg.root_hash")
if fileManager.fileExists(atPath: completionMarker.path) {
try fileManager.removeItem(at: completionMarker)
@ -92,10 +103,40 @@ final class DeveloperDiskImageService {
progressHandler?(completedStages / totalStages, "\(item.name) ready")
}
try Self.mountMethod.rawValue.write(to: Self.mountMethodURL, atomically: true, encoding: .utf8)
progressHandler?(1.0, "DDI download complete.")
}
private static let downloadItems: [DDIDownloadItem] = [
private func removeCryptexOnlyFiles() throws {
for fileName in Self.cryptexOnlyFileNames {
let fileURL = Self.directoryURL.appendingPathComponent(fileName)
if fileManager.fileExists(atPath: fileURL.path) {
try fileManager.removeItem(at: fileURL)
}
}
}
private static var mountMethod: DDIMountMethod {
let version = ProcessInfo.processInfo.operatingSystemVersion
return version.majorVersion > 26 || (version.majorVersion == 26 && version.minorVersion >= 4)
? .cryptex
: .personalized
}
private static let mountMethodURL = directoryURL.appendingPathComponent("MountMethod")
private static let cryptexOnlyFileNames = ["Image.dmg.cryptex_info", "Image.dmg.root_hash"]
private static var downloadItems: [DDIDownloadItem] {
switch mountMethod {
case .cryptex:
return cryptexDownloadItems
case .personalized:
return personalizedDownloadItems
}
}
private static let cryptexDownloadItems: [DDIDownloadItem] = [
.init(
name: "Build Manifest",
fileName: "BuildManifest.plist",
@ -122,6 +163,29 @@ final class DeveloperDiskImageService {
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.root_hash"
)
]
private static let personalizedDownloadItems: [DDIDownloadItem] = [
.init(
name: "Build Manifest",
fileName: "BuildManifest.plist",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist"
),
.init(
name: "Image",
fileName: "Image.dmg",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg"
),
.init(
name: "TrustCache",
fileName: "Image.dmg.trustcache",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg.trustcache"
)
]
}
private enum DDIMountMethod: String {
case cryptex
case personalized
}
private struct DDIDownloadItem {