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:
zqxwce
2026-08-12 11:40:24 +03:00
committed by zqxwce
co-authored by Claude Fable 5
parent fdf9487bfd
commit 74b6076557
6 changed files with 82 additions and 17 deletions
+9 -5
View File
@@ -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
+15 -1
View File
@@ -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]
+3 -1
View File
@@ -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)")
}
+2 -1
View File
@@ -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))