mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-02 02:34:29 +00:00
cli: Wire --frida through the pipeline, gated to cloudOS 26.4+
Expose --frida on patch-firmware, patch-component, fw patch, and vm create (plus `make fw_patch_jb/exp FRIDA=1`), threading it into KernelJBPatcher.applyFrida via FirmwarePipeline. The kernel patches apply only when the cloudOS kernel is 26.4+ (the versions where the shapes were validated); older kernels are left untouched. Baseline JB/EXP output is byte-identical without --frida (26.4 emits 83 records, 87 with). Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
committed by
zqxwce
co-authored by
Claude Fable 5
parent
fdf9487bfd
commit
74b6076557
@@ -112,8 +112,10 @@ help:
|
||||
@echo " make fw_patch_dev Patch boot chain with Swift pipeline (dev mode TXM patches)"
|
||||
@echo " make fw_patch_jb Patch boot chain with Swift pipeline (dev + JB extensions)"
|
||||
@echo " Options: FORCE_EXC_GUARD=1 (see fw_patch above)"
|
||||
@echo " FRIDA=1 Opt in to the Frida Stalker kernel relaxations"
|
||||
@echo " make fw_patch_exp Patch boot chain with Swift pipeline (JB + EXP experimental)"
|
||||
@echo " Options: FORCE_EXC_GUARD=1 (see fw_patch above)"
|
||||
@echo " FRIDA=1 Opt in to the Frida Stalker kernel relaxations"
|
||||
@echo ""
|
||||
@echo "Testing:"
|
||||
@echo " make test_jb_patches Run all JB kernel patches (incl. Sandbox) over every supported cloudOS kernel"
|
||||
@@ -369,11 +371,13 @@ fw_patch_dev: patcher_build
|
||||
|
||||
fw_patch_jb: patcher_build
|
||||
"$(CURDIR)/$(PATCHER_BINARY)" patch-firmware --vm-directory "$(VM_DIR_ABS)" --variant jb \
|
||||
$(if $(filter 1 true yes YES TRUE,$(FORCE_EXC_GUARD)),--force-exc-guard,)
|
||||
$(if $(filter 1 true yes YES TRUE,$(FORCE_EXC_GUARD)),--force-exc-guard,) \
|
||||
$(if $(filter 1 true yes YES TRUE,$(FRIDA)),--frida,)
|
||||
|
||||
fw_patch_exp: patcher_build
|
||||
"$(CURDIR)/$(PATCHER_BINARY)" patch-firmware --vm-directory "$(VM_DIR_ABS)" --variant exp \
|
||||
$(if $(filter 1 true yes YES TRUE,$(FORCE_EXC_GUARD)),--force-exc-guard,)
|
||||
$(if $(filter 1 true yes YES TRUE,$(FORCE_EXC_GUARD)),--force-exc-guard,) \
|
||||
$(if $(filter 1 true yes YES TRUE,$(FRIDA)),--frida,)
|
||||
|
||||
.PHONY: test_jb_patches
|
||||
|
||||
@@ -481,13 +485,13 @@ cfw_install_dev:
|
||||
$(MAKE) cfw_install_host VARIANT=dev
|
||||
|
||||
cfw_install_jb:
|
||||
$(MAKE) cfw_install_host VARIANT=jb
|
||||
$(MAKE) cfw_install_host VARIANT=jb FRIDA="$(FRIDA)"
|
||||
|
||||
cfw_install_exp:
|
||||
$(MAKE) cfw_install_host VARIANT=exp SPOOF_BUILD="$(SPOOF_BUILD)"
|
||||
$(MAKE) cfw_install_host VARIANT=exp SPOOF_BUILD="$(SPOOF_BUILD)" FRIDA="$(FRIDA)"
|
||||
|
||||
# CFW install: place files via host mount + flip the boot snapshot offline.
|
||||
# VM must be off; re-execs under sudo.
|
||||
# Options: VARIANT=regular|dev|jb|exp (default exp) SPOOF_BUILD=<id> (exp)
|
||||
cfw_install_host:
|
||||
$(if $(SPOOF_BUILD),SPOOF_BUILD="$(SPOOF_BUILD)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_host.sh" --variant $(if $(VARIANT),$(VARIANT),exp) "$(VM_DIR_ABS)"
|
||||
$(if $(SPOOF_BUILD),SPOOF_BUILD="$(SPOOF_BUILD)") $(if $(filter 1 true yes YES TRUE,$(FRIDA)),VPHONE_FRIDA=1) zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_host.sh" --variant $(if $(VARIANT),$(VARIANT),exp) "$(VM_DIR_ABS)"
|
||||
|
||||
@@ -81,6 +81,7 @@ public final class FirmwarePipeline {
|
||||
let noBinpack: Bool
|
||||
let noVphoned: Bool
|
||||
let forceExcGuard: Bool
|
||||
let enableFrida: Bool
|
||||
let loader: any FirmwareLoader
|
||||
|
||||
/// Set when the iPhone base is iOS 18.x (read from iPhone-BuildManifest.plist).
|
||||
@@ -93,6 +94,9 @@ public final class FirmwarePipeline {
|
||||
/// byte-identical to pre-branch. Computed in `patchAll()` alongside iosBaseIs18.
|
||||
private var iosBaseIs27 = false
|
||||
|
||||
/// Set when the cloudOS kernel is 26.4+; gates the opt-in Frida kernel patches.
|
||||
private var cloudOSIsFridaCapable = false
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public init(
|
||||
@@ -102,6 +106,7 @@ public final class FirmwarePipeline {
|
||||
noBinpack: Bool = false,
|
||||
noVphoned: Bool = false,
|
||||
forceExcGuard: Bool = false,
|
||||
enableFrida: Bool = false,
|
||||
loader: (any FirmwareLoader)? = nil
|
||||
) {
|
||||
self.vmDirectory = vmDirectory
|
||||
@@ -110,6 +115,7 @@ public final class FirmwarePipeline {
|
||||
self.noBinpack = noBinpack
|
||||
self.noVphoned = noVphoned
|
||||
self.forceExcGuard = forceExcGuard
|
||||
self.enableFrida = enableFrida
|
||||
self.loader = loader ?? ContainerFirmwareLoader()
|
||||
}
|
||||
|
||||
@@ -135,6 +141,17 @@ public final class FirmwarePipeline {
|
||||
: iosBaseIs27 ? " (enabling iOS-27 JB kernel patches)" : ""
|
||||
log("[*] iPhone base iOS: \(baseVersion ?? "unknown")\(baseGateNote)")
|
||||
|
||||
// Frida Stalker kernel patches only apply on cloudOS 26.4+ (where the shapes
|
||||
// were validated); older kernels are left untouched. The Frida deb install is
|
||||
// separate and version-independent.
|
||||
let cloudOSVersion = Self.readCloudOSProductVersion(restoreDir)
|
||||
cloudOSIsFridaCapable = Self.productVersionAtLeast(cloudOSVersion, 26, 4)
|
||||
if enableFrida {
|
||||
log("[*] cloudOS kernel: \(cloudOSVersion ?? "unknown")"
|
||||
+ (cloudOSIsFridaCapable ? " (Frida kernel patches enabled)"
|
||||
: " (< 26.4 — Frida kernel patches skipped)"))
|
||||
}
|
||||
|
||||
let components = buildComponentList()
|
||||
log("[*] Patching \(components.count) boot-chain components ...")
|
||||
|
||||
@@ -220,6 +237,9 @@ public final class FirmwarePipeline {
|
||||
// JB kernel patches so 18.x/26.x bases apply none of them.
|
||||
let applyIOS27 = iosBaseIs27
|
||||
|
||||
// Opt-in Frida Stalker kernel relaxations (--frida), gated to cloudOS 26.4+.
|
||||
let applyFrida = enableFrida && cloudOSIsFridaCapable
|
||||
|
||||
// iOS 18 bases: disable the skywalk flowswitch netagents via boot-arg so
|
||||
// Network.framework uses the BSD path (the 26.1-kernel skywalk
|
||||
// channel-create traps in the 18.x Network.framework and crash-loops
|
||||
@@ -341,6 +361,7 @@ public final class FirmwarePipeline {
|
||||
{ data, verbose in
|
||||
let p = KernelJBPatcher(data: data, verbose: verbose)
|
||||
p.applyIOS27 = applyIOS27
|
||||
p.applyFrida = applyFrida
|
||||
return p
|
||||
},
|
||||
]
|
||||
@@ -352,6 +373,7 @@ public final class FirmwarePipeline {
|
||||
{ data, verbose in
|
||||
let p = KernelJBPatcher(data: data, verbose: verbose)
|
||||
p.applyIOS27 = applyIOS27
|
||||
p.applyFrida = applyFrida
|
||||
return p
|
||||
},
|
||||
{ data, verbose in
|
||||
@@ -436,12 +458,9 @@ public final class FirmwarePipeline {
|
||||
return restoreDir
|
||||
}
|
||||
|
||||
/// Read the iPhone base `ProductVersion` from `iPhone-BuildManifest.plist`
|
||||
/// (preserved by fw_prepare before the hybrid manifest overwrites
|
||||
/// BuildManifest.plist). Returns nil if absent/unreadable — callers then
|
||||
/// treat the base as non-iOS-18 (conservative).
|
||||
static func readBaseProductVersion(_ restoreDir: URL) -> String? {
|
||||
let url = restoreDir.appendingPathComponent("iPhone-BuildManifest.plist")
|
||||
/// `ProductVersion` from a manifest in `restoreDir`, or nil if absent/unreadable.
|
||||
static func readProductVersion(_ restoreDir: URL, manifest: String) -> String? {
|
||||
let url = restoreDir.appendingPathComponent(manifest)
|
||||
guard let data = try? Data(contentsOf: url),
|
||||
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil),
|
||||
let dict = plist as? [String: Any],
|
||||
@@ -450,6 +469,23 @@ public final class FirmwarePipeline {
|
||||
return version
|
||||
}
|
||||
|
||||
/// iPhone base version (`iPhone-BuildManifest.plist`, preserved by fw_prepare).
|
||||
static func readBaseProductVersion(_ restoreDir: URL) -> String? {
|
||||
readProductVersion(restoreDir, manifest: "iPhone-BuildManifest.plist")
|
||||
}
|
||||
|
||||
/// cloudOS/kernel version (the live `BuildManifest.plist`).
|
||||
static func readCloudOSProductVersion(_ restoreDir: URL) -> String? {
|
||||
readProductVersion(restoreDir, manifest: "BuildManifest.plist")
|
||||
}
|
||||
|
||||
/// Dotted `ProductVersion` >= major.minor, compared numerically. nil is false.
|
||||
static func productVersionAtLeast(_ version: String?, _ major: Int, _ minor: Int) -> Bool {
|
||||
guard let parts = version?.split(separator: ".").compactMap({ Int($0) }),
|
||||
let vMajor = parts.first else { return false }
|
||||
return vMajor != major ? vMajor > major : (parts.count > 1 ? parts[1] : 0) >= minor
|
||||
}
|
||||
|
||||
private func compareRestoreDirectories(_ lhs: URL, _ rhs: URL) -> Bool {
|
||||
let leftName = lhs.lastPathComponent
|
||||
let rightName = rhs.lastPathComponent
|
||||
|
||||
@@ -187,6 +187,12 @@ struct PatchFirmwareCLI: ParsableCommand {
|
||||
)
|
||||
var forceExcGuard: Bool = false
|
||||
|
||||
@Flag(
|
||||
name: .customLong("frida"),
|
||||
help: "Opt in to Frida Stalker kernel relaxations (existing-thread follow + repeated VM_PROT_COPY). jb/exp only."
|
||||
)
|
||||
var frida: Bool = false
|
||||
|
||||
mutating func run() throws {
|
||||
let pipeline = FirmwarePipeline(
|
||||
vmDirectory: vmDirectory,
|
||||
@@ -194,7 +200,8 @@ struct PatchFirmwareCLI: ParsableCommand {
|
||||
verbose: !quiet,
|
||||
noBinpack: noBinpack,
|
||||
noVphoned: noVphoned,
|
||||
forceExcGuard: forceExcGuard
|
||||
forceExcGuard: forceExcGuard,
|
||||
enableFrida: frida
|
||||
)
|
||||
let records = try pipeline.patchAll()
|
||||
|
||||
@@ -259,6 +266,12 @@ struct PatchComponentCLI: ParsableCommand {
|
||||
)
|
||||
var targetOS: String?
|
||||
|
||||
@Flag(
|
||||
name: .customLong("frida"),
|
||||
help: "kernel-jb only: opt in to the Frida Stalker kernel relaxations."
|
||||
)
|
||||
var frida: Bool = false
|
||||
|
||||
mutating func run() throws {
|
||||
let payload = try IM4PHandler.load(contentsOf: input).payload
|
||||
let count: Int
|
||||
@@ -288,6 +301,7 @@ struct PatchComponentCLI: ParsableCommand {
|
||||
// --target-os, default to applying them so the dev/test tool exercises the
|
||||
// full set.
|
||||
patcher.applyIOS27 = targetOS.map { $0.hasPrefix("27.") } ?? true
|
||||
patcher.applyFrida = frida
|
||||
count = try patcher.apply()
|
||||
patchedData = patcher.buffer.data
|
||||
records = patcher.patches
|
||||
|
||||
@@ -99,6 +99,7 @@ public struct VPhoneCreateOrchestrator {
|
||||
public var sudoPassword: String?
|
||||
public var spoofBuild: String?
|
||||
public var forceDSCMaxSlide: Bool
|
||||
public var enableFrida: Bool
|
||||
public var rootPopup: Bool
|
||||
public var interactive: Bool
|
||||
public var cpuCount: UInt
|
||||
@@ -115,6 +116,7 @@ public struct VPhoneCreateOrchestrator {
|
||||
sudoPassword: String? = nil,
|
||||
spoofBuild: String? = nil,
|
||||
forceDSCMaxSlide: Bool = false,
|
||||
enableFrida: Bool = false,
|
||||
rootPopup: Bool = false,
|
||||
interactive: Bool = false,
|
||||
cpuCount: UInt = 8,
|
||||
@@ -130,6 +132,7 @@ public struct VPhoneCreateOrchestrator {
|
||||
self.sudoPassword = sudoPassword
|
||||
self.spoofBuild = spoofBuild
|
||||
self.forceDSCMaxSlide = forceDSCMaxSlide
|
||||
self.enableFrida = enableFrida
|
||||
self.rootPopup = rootPopup
|
||||
self.interactive = interactive
|
||||
self.cpuCount = cpuCount
|
||||
@@ -209,7 +212,9 @@ public struct VPhoneCreateOrchestrator {
|
||||
try runFWPrepare(options: options, isLess: isLess, bundleURL: bundleURL)
|
||||
|
||||
print("\n=== fw patch ===")
|
||||
try runFWPatch(variant: variantOption, isLess: isLess, bundleURL: bundleURL, verbosity: v)
|
||||
try runFWPatch(
|
||||
variant: variantOption, isLess: isLess, enableFrida: options.enableFrida,
|
||||
bundleURL: bundleURL, verbosity: v)
|
||||
|
||||
print("\n=== Restore phase ===")
|
||||
try runRestorePhase(bundleURL: bundleURL, verbosity: v)
|
||||
@@ -326,7 +331,8 @@ public struct VPhoneCreateOrchestrator {
|
||||
}
|
||||
|
||||
private func runFWPatch(
|
||||
variant: PatchFirmwareCLI.VariantOption, isLess: Bool, bundleURL: URL, verbosity v: VPhoneVerbosity
|
||||
variant: PatchFirmwareCLI.VariantOption, isLess: Bool, enableFrida: Bool,
|
||||
bundleURL: URL, verbosity v: VPhoneVerbosity
|
||||
) throws {
|
||||
// Mirrors the Makefile's `ifeq ($(UID),0)` gate on `fw_patch_less` —
|
||||
// only the `less` variant requires root.
|
||||
@@ -343,7 +349,8 @@ public struct VPhoneCreateOrchestrator {
|
||||
trace("in-process FirmwarePipeline.patchAll variant=\(variant.rawValue)", v)
|
||||
let pipeline = FirmwarePipeline(
|
||||
vmDirectory: bundleURL, variant: variant.pipelineVariant, verbose: v.showsToolDetail,
|
||||
noBinpack: false, noVphoned: false, forceExcGuard: false)
|
||||
noBinpack: false, noVphoned: false, forceExcGuard: false,
|
||||
enableFrida: enableFrida)
|
||||
let records = try pipeline.patchAll()
|
||||
print("[fw patch] applied \(records.count) patches for \(variant.rawValue)")
|
||||
}
|
||||
@@ -490,6 +497,7 @@ public struct VPhoneCreateOrchestrator {
|
||||
]
|
||||
if let spoofBuild = options.spoofBuild { scriptEnv["SPOOF_BUILD"] = spoofBuild }
|
||||
if options.forceDSCMaxSlide { scriptEnv["FORCE_DSC_MAXSLIDE"] = "1" }
|
||||
if options.enableFrida { scriptEnv["VPHONE_FRIDA"] = "1" }
|
||||
if options.keepArtifacts { scriptEnv["VPHONE_KEEP_ARTIFACTS"] = "1" }
|
||||
|
||||
let args = [resources.cfwInstallHostScript.path, "--variant", options.variant, bundleURL.path]
|
||||
|
||||
@@ -95,6 +95,7 @@ struct VPhoneFWPatchCommand: ParsableCommand {
|
||||
@Argument(help: "VM name") var name: String?
|
||||
@Option(name: [.customShort("V"), .long], help: "variant: regular | dev | jb | exp | less") var variant: PatchFirmwareCLI.VariantOption = .regular
|
||||
@Flag(name: .customLong("force-exc-guard"), help: "Force the EXC_GUARD disable patch") var forceExcGuard = false
|
||||
@Flag(name: .customLong("frida"), help: "Opt in to Frida Stalker kernel relaxations (jb/exp only)") var frida = false
|
||||
@Flag(name: .shortAndLong, help: "Suppress per-component progress") var quiet = false
|
||||
|
||||
func run() throws {
|
||||
@@ -114,7 +115,8 @@ struct VPhoneFWPatchCommand: ParsableCommand {
|
||||
verbose: !quiet,
|
||||
noBinpack: false,
|
||||
noVphoned: false,
|
||||
forceExcGuard: forceExcGuard)
|
||||
forceExcGuard: forceExcGuard,
|
||||
enableFrida: frida)
|
||||
let records = try pipeline.patchAll()
|
||||
print("[fw patch] applied \(records.count) patches for \(variant.rawValue)")
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ struct VPhoneVMCreateCommand: ParsableCommand {
|
||||
var sudoPassword: String?
|
||||
@Option(name: [.customShort("b"), .long], help: "(exp only) rewrite ProductBuildVersion to this build id") var spoofBuild: String?
|
||||
@Flag(name: .customLong("force-dsc-maxslide"), help: "Zero the dyld cache maxSlide on non-27 bases (opt-in DSC-map fit)") var forceDSCMaxSlide = false
|
||||
@Flag(name: .customLong("frida"), help: "Opt in to Frida Stalker support: install re.frida.server (latest GitHub release) + jb/exp kernel relaxations") var frida = false
|
||||
@Flag(name: .customLong("root-popup"), help: "Elevate the CFW host-mount via macOS's native authentication dialog (osascript) instead of a sudo prompt") var rootPopup = false
|
||||
@Flag(help: "Prompt at first-boot stages instead of running non-interactively") var interactive = false
|
||||
@Flag(name: .customLong("keep-artifacts"), help: "Keep intermediate build artifacts (built restore firmware, extracted base-IPSW caches, extracted CFW input dirs) instead of removing them after use. Source archives (.ipsw / .tar.zst) are always kept.")
|
||||
@@ -41,7 +42,7 @@ struct VPhoneVMCreateCommand: ParsableCommand {
|
||||
name: name, variant: variant,
|
||||
iphoneSource: sources.iphoneSource, cloudosSource: sources.cloudosSource,
|
||||
sudoPassword: sudoPassword, spoofBuild: spoofBuild, forceDSCMaxSlide: forceDSCMaxSlide,
|
||||
rootPopup: rootPopup,
|
||||
enableFrida: frida, rootPopup: rootPopup,
|
||||
interactive: interactive, diskSizeGB: diskSize,
|
||||
verbosity: VPhoneVerbosity(count: verboseCount),
|
||||
keepArtifacts: keepArtifacts))
|
||||
|
||||
Reference in New Issue
Block a user