vphone-cli: Add --root-popup to elevate CFW host-mount via macOS auth dialog

* feat: add --root-popup to elevate CFW host-mount via macOS auth dialog

Adds --root-popup to `cfw install` and `vm create`, elevating the CFW host-mount through macOS's native authentication dialog (osascript -> do shell script with administrator privileges) instead of the script's sudo re-exec. do shell script runs under a bare env, so the vars the bundled scripts read are forwarded inline, plus SUDO_USER so the script's chown-back still returns artifacts to the invoking user. On `vm create`, --sudo-password takes precedence.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* cfw: remove entire .cfw_temp on install cleanup

Replaces the selective `rm -f` of individual temp binaries with `rm -rf "$TEMP_DIR"`, dropping the cached Cryptex DMGs along with the temp files.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

---------

Co-authored-by: Claude Opus 4.8 <[email protected]>
This commit is contained in:
zqxwce
2026-08-02 11:20:22 +03:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 744090f694
commit 6aef60bd9a
8 changed files with 94 additions and 64 deletions
+2 -8
View File
@@ -579,14 +579,8 @@ echo "[*] Unmounting image volumes..."
/sbin/umount $MNT1 2>/dev/null || true
/sbin/umount $MNT3 2>/dev/null || true
# Keep .cfw_temp/Cryptex*.dmg cached (slow to re-create)
# Only remove temp binaries
echo "[*] Cleaning up temp binaries..."
rm -f "$TEMP_DIR/seputil" \
"$TEMP_DIR/launchd_cache_loader" \
"$TEMP_DIR/mobileactivationd" \
"$TEMP_DIR/vphoned" \
"$TEMP_DIR/launchd.plist"
echo "[*] Cleaning up temp..."
rm -rf "$TEMP_DIR"
echo ""
echo "[+] CFW installation complete!"
+2 -8
View File
@@ -508,14 +508,8 @@ echo "[*] Unmounting image volumes..."
/sbin/umount $MNT1 2>/dev/null || true
/sbin/umount $MNT3 2>/dev/null || true
# Keep .cfw_temp/Cryptex*.dmg cached (slow to re-create)
# Only remove temp binaries
echo "[*] Cleaning up temp binaries..."
rm -f "$TEMP_DIR/seputil" \
"$TEMP_DIR/launchd_cache_loader" \
"$TEMP_DIR/mobileactivationd" \
"$TEMP_DIR/vphoned" \
"$TEMP_DIR/launchd.plist"
echo "[*] Cleaning up temp..."
rm -rf "$TEMP_DIR"
echo ""
echo "[+] CFW installation complete!"
+2 -3
View File
@@ -818,9 +818,8 @@ echo "[*] Unmounting image volumes..."
/sbin/umount $MNT3 2>/dev/null || true
/sbin/umount $MNT5 2>/dev/null || true
echo "[*] Cleaning up temp binaries..."
rm -f "$TEMP_DIR/launchd" \
"$TEMP_DIR/bootstrap-iphoneos-arm64.tar"
echo "[*] Cleaning up temp..."
rm -rf "$TEMP_DIR"
echo ""
echo "[+] CFW + JB + EXP installation complete!"
+2 -3
View File
@@ -480,9 +480,8 @@ echo "[*] Unmounting image volumes..."
/sbin/umount $MNT3 2>/dev/null || true
/sbin/umount $MNT5 2>/dev/null || true
echo "[*] Cleaning up temp binaries..."
rm -f "$TEMP_DIR/launchd" \
"$TEMP_DIR/bootstrap-iphoneos-arm64.tar"
echo "[*] Cleaning up temp..."
rm -rf "$TEMP_DIR"
echo ""
echo "[+] CFW + JB installation complete!"
@@ -167,4 +167,23 @@ public enum VPhoneProcessRunner {
if savedFg > 0 { _ = tcsetpgrp(ttyFD, savedFg) } // take it back
return process.terminationStatus
}
/// Run `executable args` as root via macOS's native auth dialog (`osascript`
/// `do shell script with administrator privileges`). `do shell script` runs
/// under a bare env, so `env` is passed inline as `KEY=value` nothing else
/// reaches the command. Returns the command's exit status.
public static func runWithAdminPrivileges(
_ executable: URL, _ args: [String], env: [String: String] = [:], echo: Bool = true
) throws -> Int32 {
func shQuote(_ s: String) -> String { "'" + s.replacingOccurrences(of: "'", with: "'\\''") + "'" }
var tokens = env.sorted { $0.key < $1.key }.map { "\($0.key)=\(shQuote($0.value))" }
tokens.append(shQuote(executable.path))
tokens += args.map(shQuote)
// Escape the /bin/sh command for the AppleScript string literal (\ then ").
let appleEscaped = tokens.joined(separator: " ")
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
let source = "do shell script \"\(appleEscaped)\" with administrator privileges"
return try runStreaming(URL(fileURLWithPath: "/usr/bin/osascript"), ["-e", source], echo: echo)
}
}
@@ -99,6 +99,7 @@ public struct VPhoneCreateOrchestrator {
public var sudoPassword: String?
public var spoofBuild: String?
public var forceDSCMaxSlide: Bool
public var rootPopup: Bool
public var interactive: Bool
public var cpuCount: UInt
public var memoryMB: UInt64
@@ -114,6 +115,7 @@ public struct VPhoneCreateOrchestrator {
sudoPassword: String? = nil,
spoofBuild: String? = nil,
forceDSCMaxSlide: Bool = false,
rootPopup: Bool = false,
interactive: Bool = false,
cpuCount: UInt = 8,
memoryMB: UInt64 = 8192,
@@ -128,6 +130,7 @@ public struct VPhoneCreateOrchestrator {
self.sudoPassword = sudoPassword
self.spoofBuild = spoofBuild
self.forceDSCMaxSlide = forceDSCMaxSlide
self.rootPopup = rootPopup
self.interactive = interactive
self.cpuCount = cpuCount
self.memoryMB = memoryMB
@@ -187,9 +190,9 @@ public struct VPhoneCreateOrchestrator {
} else {
print("[!] --sudo-password failed validation; will still try at CFW-install time")
}
} else if isatty(FileHandle.standardInput.fileDescriptor) == 0 {
// No password and no terminal for sudo to prompt on fail before
// the long download/restore, not at the eventual sudo prompt.
} else if !options.rootPopup && isatty(FileHandle.standardInput.fileDescriptor) == 0 {
// No password, no popup, no terminal for sudo to prompt on fail
// before the long download/restore, not at the eventual sudo prompt.
throw VPhoneCreateError.sudoPasswordRequired
}
}
@@ -475,35 +478,46 @@ public struct VPhoneCreateOrchestrator {
private func runCFWInstall(options: Options, bundleURL: URL, sudoEnvExtras: [String: String]) throws {
let v = options.verbosity
var env = ProcessInfo.processInfo.environment
if let spoofBuild = options.spoofBuild { env["SPOOF_BUILD"] = spoofBuild }
if options.forceDSCMaxSlide { env["FORCE_DSC_MAXSLIDE"] = "1" }
try FileManager.default.createDirectory(at: resources.ipswCacheDir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: resources.sealVolumeCacheDir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: resources.debsCacheDir, withIntermediateDirectories: true)
env["VPHONE_PYTHON"] = try resources.pythonExecutable().path
env["IPSW_DIR"] = resources.ipswCacheDir.path
env["VPHONE_SEAL_DIR"] = resources.sealVolumeCacheDir.path
env["VPHONE_DEBS_DIR"] = resources.debsCacheDir.path
if options.keepArtifacts { env["VPHONE_KEEP_ARTIFACTS"] = "1" }
for (key, value) in sudoEnvExtras { env[key] = value }
var scriptEnv: [String: String] = [
"VPHONE_PYTHON": try resources.pythonExecutable().path,
"IPSW_DIR": resources.ipswCacheDir.path,
"VPHONE_SEAL_DIR": resources.sealVolumeCacheDir.path,
"VPHONE_DEBS_DIR": resources.debsCacheDir.path,
]
if let spoofBuild = options.spoofBuild { scriptEnv["SPOOF_BUILD"] = spoofBuild }
if options.forceDSCMaxSlide { scriptEnv["FORCE_DSC_MAXSLIDE"] = "1" }
if options.keepArtifacts { scriptEnv["VPHONE_KEEP_ARTIFACTS"] = "1" }
let args = [resources.cfwInstallHostScript.path, "--variant", options.variant, bundleURL.path]
let envKeys = (["VPHONE_PYTHON", "IPSW_DIR", "VPHONE_SEAL_DIR"] + sudoEnvExtras.keys.sorted()).joined(separator: ", ")
trace("spawn /bin/zsh \(args.joined(separator: " ")) (env keys: \(envKeys))", v)
// With an askpass credential sudo is non-interactive honor verbosity.
// Without one, sudo must prompt on the terminal run as a foreground job
// so its process group owns the tty (see runForeground).
// --sudo-password (askpass) wins over --root-popup.
let usePopup = options.rootPopup && sudoEnvExtras["SUDO_ASKPASS"] == nil
let code: Int32
if sudoEnvExtras["SUDO_ASKPASS"] != nil {
code = try VPhoneProcessRunner.runStreaming(
URL(fileURLWithPath: "/bin/zsh"), args, env: env, echo: v.showsToolDetail)
if usePopup {
// Forward SUDO_USER (sudo would set it) so the script's chown-back runs.
scriptEnv["SUDO_USER"] = NSUserName()
trace("osascript admin-privileges /bin/zsh \(args.joined(separator: " "))", v)
code = try VPhoneProcessRunner.runWithAdminPrivileges(
URL(fileURLWithPath: "/bin/zsh"), args, env: scriptEnv, echo: v.showsToolDetail)
} else {
print("[*] CFW install needs root — sudo will prompt for your macOS password.")
// Foreground so sudo can own the tty; echo honors verbosity (quiet
// suppresses the install's own output, sudo's /dev/tty prompt stays).
code = try VPhoneProcessRunner.runForeground(
URL(fileURLWithPath: "/bin/zsh"), args, env: env, echo: v.showsToolDetail)
var env = ProcessInfo.processInfo.environment
for (key, value) in scriptEnv { env[key] = value }
for (key, value) in sudoEnvExtras { env[key] = value }
let envKeys = (["VPHONE_PYTHON", "IPSW_DIR", "VPHONE_SEAL_DIR"] + sudoEnvExtras.keys.sorted()).joined(separator: ", ")
trace("spawn /bin/zsh \(args.joined(separator: " ")) (env keys: \(envKeys))", v)
// With an askpass credential sudo is non-interactive honor verbosity.
// Without one, sudo must prompt on the terminal run as a foreground
// job so its process group owns the tty (see runForeground).
if sudoEnvExtras["SUDO_ASKPASS"] != nil {
code = try VPhoneProcessRunner.runStreaming(
URL(fileURLWithPath: "/bin/zsh"), args, env: env, echo: v.showsToolDetail)
} else {
print("[*] CFW install needs root — sudo will prompt for your macOS password.")
code = try VPhoneProcessRunner.runForeground(
URL(fileURLWithPath: "/bin/zsh"), args, env: env, echo: v.showsToolDetail)
}
}
guard code == 0 else { throw VPhoneCreateError.cfwInstallFailed(code) }
print("[+] CFW installed (\(options.variant)).")
+26 -17
View File
@@ -104,6 +104,7 @@ struct VPhoneCFWInstallCommand: ParsableCommand {
@Option(name: [.customShort("V"), .long], help: "variant: regular | dev | jb | exp") var variant: String = "exp"
@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("root-popup"), help: "Elevate via macOS's native authentication dialog (osascript) instead of the sudo re-exec") var rootPopup = false
@Flag(name: .customLong("keep-artifacts"), help: "Keep the extracted CFW input dirs (cfw_input/, cfw_jb_input/) after install (default: removed to save space)") var keepArtifacts = false
@Option(name: .shortAndLong, help: "Resource base override (default: inferred from the running binary path)")
var projectRoot: String?
@@ -119,29 +120,37 @@ struct VPhoneCFWInstallCommand: ParsableCommand {
let bundle = try lib.library.bundle(named: name)
let resources = projectRoot.map { VPhoneResources(base: URL(fileURLWithPath: $0)) } ?? .resolve()
var env = ProcessInfo.processInfo.environment
if let spoofBuild { env["SPOOF_BUILD"] = spoofBuild }
if forceDSCMaxSlide { env["FORCE_DSC_MAXSLIDE"] = "1" }
if keepArtifacts { env["VPHONE_KEEP_ARTIFACTS"] = "1" }
// Same redirect as `fw prepare`: VPHONE_PYTHON/IPSW_DIR/VPHONE_SEAL_DIR are
// exported for the bundled scripts (cfw_install_host.sh's PY/P lines honor
// VPHONE_PYTHON); the apfs_sealvolume read itself only happens on the
// `fw patch` path (CryptexFilesystemPatcher).
// Env the bundled scripts read: rides along via `sudo -E` by default;
// --root-popup forwards it inline (do shell script's bare env).
try FileManager.default.createDirectory(at: resources.ipswCacheDir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: resources.sealVolumeCacheDir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: resources.debsCacheDir, withIntermediateDirectories: true)
env["VPHONE_PYTHON"] = try resources.pythonExecutable().path
env["IPSW_DIR"] = resources.ipswCacheDir.path
env["VPHONE_SEAL_DIR"] = resources.sealVolumeCacheDir.path
env["VPHONE_DEBS_DIR"] = resources.debsCacheDir.path
var scriptEnv: [String: String] = [
"VPHONE_PYTHON": try resources.pythonExecutable().path,
"IPSW_DIR": resources.ipswCacheDir.path,
"VPHONE_SEAL_DIR": resources.sealVolumeCacheDir.path,
"VPHONE_DEBS_DIR": resources.debsCacheDir.path,
]
if let spoofBuild { scriptEnv["SPOOF_BUILD"] = spoofBuild }
if forceDSCMaxSlide { scriptEnv["FORCE_DSC_MAXSLIDE"] = "1" }
if keepArtifacts { scriptEnv["VPHONE_KEEP_ARTIFACTS"] = "1" }
let args = [resources.cfwInstallHostScript.path, "--variant", variant, bundle.url.path]
if v.tracesInternals {
print("[trace] spawning: /bin/zsh \(args.joined(separator: " ")) (env keys: VPHONE_PYTHON, IPSW_DIR, VPHONE_SEAL_DIR)")
let code: Int32
if rootPopup {
// Forward SUDO_USER (sudo would set it) so the script's chown-back runs.
scriptEnv["SUDO_USER"] = NSUserName()
code = try VPhoneProcessRunner.runWithAdminPrivileges(
URL(fileURLWithPath: "/bin/zsh"), args, env: scriptEnv, echo: v.showsToolDetail)
} else {
var env = ProcessInfo.processInfo.environment
for (key, value) in scriptEnv { env[key] = value }
if v.tracesInternals {
print("[trace] spawning: /bin/zsh \(args.joined(separator: " ")) (env keys: VPHONE_PYTHON, IPSW_DIR, VPHONE_SEAL_DIR)")
}
code = try VPhoneProcessRunner.runStreaming(
URL(fileURLWithPath: "/bin/zsh"), args, env: env, echo: v.showsToolDetail)
}
let code = try VPhoneProcessRunner.runStreaming(
URL(fileURLWithPath: "/bin/zsh"), args, env: env, echo: v.showsToolDetail)
if code == 0, !keepArtifacts, let removed = try? VPhoneRestoreInfo.removeBuiltFirmware(fromBundle: bundle) {
print("[cfw] removed built firmware \(removed)/ to save space (--keep-artifacts to keep)")
}
@@ -20,6 +20,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("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.")
var keepArtifacts = false
@@ -39,6 +40,7 @@ struct VPhoneVMCreateCommand: ParsableCommand {
name: name, variant: variant,
iphoneSource: sources.iphoneSource, cloudosSource: sources.cloudosSource,
sudoPassword: sudoPassword, spoofBuild: spoofBuild, forceDSCMaxSlide: forceDSCMaxSlide,
rootPopup: rootPopup,
interactive: interactive, verbosity: VPhoneVerbosity(count: verboseCount),
keepArtifacts: keepArtifacts))
}