Move guest payloads into Contents/Resources/guest-resources

VPhone.bundle kept vphoned, the guest dylibs and the GPU compiler plugin
beside the host programs in Contents/MacOS. Everything installed into the
guest now lives in Contents/Resources/guest-resources: vphoned (formerly
vphoned.signed), its launch daemon plist, the launchd hook, SystemHook,
both camera hooks with their filters, and the GPU compiler plugin.
Contents/MacOS holds only vphone-vm, vphone-cli, VPhoneEscalator and the
span compatibility library.

ValidateBundle.sh checks the platform of every Mach-O: iOS binaries only
in guest-resources, none in Contents/MacOS, and no Mach-O elsewhere. The
old Resources/guest directory and guest binaries in Contents/MacOS are
rejected. codesign --verify --strict and --deep both accept the layout,
and vphoned keeps its own entitlements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lakr233
2026-09-25 14:57:12 +07:00
co-authored by Claude Opus 5.5
parent c238b425e1
commit 961f18c075
10 changed files with 89 additions and 59 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ See `Research/` for detailed firmware pipeline, component origins, patch breakdo
- `VPhoneGuestComponents`: guest dylibs built by Makefile from the bundle build phase.
- `VPhoneLaunchpad`: `vphone-launchpad.app`, the workstation app that downloads and manages `VPhone.bundle` releases and drives VMs through the active bundle's `vphone-cli`, plus its SMJobBless helper `com.vphone.launchpad.helper`. Shipped separately from the bundle. Settings live only in `VPhoneLaunchpad/Configuration/*.xcconfig`; the pbxproj holds none.
The `VPhone` scheme puts all shipped Mach-O files in `VPhone.bundle/Contents/MacOS`. Guest configuration is in `Contents/Resources`. Xcode targets have `CODE_SIGNING_ALLOWED=NO`; the bundle build phase signs each binary ad hoc with only its own entitlements, then seals the outer bundle. `VPhoneVirtualization.entitlements` belongs to `vphone-vm`; `VPhoneDaemon.entitlements` belongs to `vphoned`. The bundle and CLI have no private entitlements.
The `VPhone` scheme puts host programs in `VPhone.bundle/Contents/MacOS` and everything installed into the guest (vphoned, guest dylibs, their plists) in `Contents/Resources/guest-resources`. Xcode targets have `CODE_SIGNING_ALLOWED=NO`; the bundle build phase signs each binary ad hoc with only its own entitlements, then seals the outer bundle. `VPhoneVirtualization.entitlements` belongs to `vphone-vm`; `VPhoneDaemon.entitlements` belongs to `vphoned`. The bundle and CLI have no private entitlements.
### Key Patterns
+9 -4
View File
@@ -11,10 +11,15 @@ executables and resources, not a macOS app or a dynamically loaded plug-in.
| `Contents/MacOS/vphone-cli` | Unentitled command entry point |
| `Contents/MacOS/vphone-vm` | VM and window process; private virtualization entitlements |
| `Contents/MacOS/VPhoneEscalator` | AMFI allowlist tool for the current VM cdhash |
| `Contents/MacOS/vphoned.signed` | Pre-signed guest daemon payload with its own entitlements; the bundle contains no unsigned copy |
| Guest dylibs in `Contents/MacOS` | Guest installation payloads |
| `Contents/Resources/guest/vphoned.plist` | Guest launch daemon configuration |
| `Contents/Resources` | Guest configuration and nonexecutable resources; no signing script or entitlement file is shipped |
| `Contents/MacOS/libswiftCompatibilitySpan.vphone.dylib` | Swift back-deployment library for `vphone-vm` on macOS 15 |
| `Contents/Resources/guest-resources/vphoned` | Pre-signed guest daemon with its own entitlements; the bundle contains no unsigned copy |
| `Contents/Resources/guest-resources/*.dylib` | Guest libraries: launchd hook, SystemHook, camera hooks and the GPU compiler plugin |
| `Contents/Resources/guest-resources/*.plist` | Guest launch daemon configuration and camera hook filters |
| `Contents/Resources` | Localized strings; no signing script or entitlement file is shipped |
`Contents/MacOS` holds only programs that run on the Mac.
`guest-resources` holds only files installed into the guest; every Mach-O in
it is built for iOS. `ValidateBundle.sh` enforces both rules.
All executable payloads use ad hoc code signatures. Only the required child
processes carry private entitlements. The bundle has no `CFBundleExecutable`,
@@ -49,7 +49,7 @@ extension CryptexFilesystemPatcher {
let launchdOgPath = target.appending(path: "/System/Library/xpc/launchd.plist")
try FileManager.default.moveItem(at: launchdOgPath, to: launchdPath)
let vphonedLaunchdPlist = resources.base.appendingPathComponent("guest/vphoned.plist")
let vphonedLaunchdPlist = resources.guestResources.appendingPathComponent("vphoned.plist")
try FileManager.default.copyItem(
at: vphonedLaunchdPlist,
to: target.appending(path: "System/Library/LaunchDaemons/vphoned.plist"),
@@ -83,7 +83,7 @@ extension CryptexFilesystemPatcher {
/// that VM. vphoned is cross-compiled at build time now
/// by the Xcode guest target and staged into the bundle.
func stageVphoned(to vphonedBin: URL) throws {
let prebuilt = try VPhoneGuestBinaries.resolve("vphoned.signed")
let prebuilt = try VPhoneGuestBinaries.resolve("vphoned")
try FileManager.default.copyItem(at: prebuilt, to: vphonedBin)
}
@@ -355,7 +355,7 @@ struct VPhoneCustomFirmwareInstaller {
// Install the same signed bytes that vm launch uses for auto-update.
// Re-signing here changes the binary hash and forces an upload and
// daemon restart on the VM's first boot.
let vphoned = try VPhoneGuestBinaries.resolve("vphoned.signed")
let vphoned = try VPhoneGuestBinaries.resolve("vphoned")
let staged = work.appendingPathComponent("vphoned")
try fm.copyItem(at: vphoned, to: staged)
try replace(staged, at: system.appendingPathComponent("usr/bin/vphoned"), mode: 0o755)
@@ -364,7 +364,7 @@ struct VPhoneCustomFirmwareInstaller {
try fm.removeItem(at: signed)
}
try fm.copyItem(at: staged, to: signed)
let daemon = resources.base.appendingPathComponent("guest/vphoned.plist")
let daemon = resources.guestResources.appendingPathComponent("vphoned.plist")
try replace(
daemon,
at: system.appendingPathComponent(
@@ -7,6 +7,9 @@ configuration="${CONFIGURATION:?}"
bundle="${TARGET_BUILD_DIR:?}/${FULL_PRODUCT_NAME:?}"
macos="$bundle/Contents/MacOS"
resources="$bundle/Contents/Resources"
# Host programs run from Contents/MacOS. Everything installed into the guest
# lives in guest-resources and never runs on the Mac.
guest="$resources/guest-resources"
# Xcode exports the bundle target's SDK and package paths to build phases. Nested
# xcodebuild must resolve each project's own graph, especially the iOS daemon.
@@ -50,19 +53,20 @@ if /usr/bin/nm -u "$daemon_products/vphoned" | /usr/bin/grep -q '_swift_initBorr
fi
/bin/rm -rf "$macos" "$resources"
/bin/mkdir -p "$macos" "$resources/guest"
/bin/mkdir -p "$macos" "$guest"
/bin/cp "$TARGET_BUILD_DIR/vphone-vm" "$macos/vphone-vm"
/bin/cp "$command_products/vphone-cli" "$macos/vphone-cli"
/bin/cp "$daemon_products/vphoned" "$macos/vphoned.signed"
/bin/cp "$amfi_products/VPhoneEscalator" "$macos/VPhoneEscalator"
/bin/cp "$guest_products/camfix/libcamfix.dylib" "$macos/libcamfix.dylib"
/bin/cp "$guest_products/vcamcaptured/libvcamcaptured.dylib" "$macos/libvcamcaptured.dylib"
/bin/cp "$guest_products/launchhook/launchdhook-vphone.dylib" "$macos/launchdhook-vphone.dylib"
/bin/cp "$guest_products/systemhook/SystemHook-vphone.dylib" "$macos/SystemHook-vphone.dylib"
/bin/cp "$guest_products/gpu/libAppleParavirtCompilerPluginIOGPUFamily.dylib" "$macos/libAppleParavirtCompilerPluginIOGPUFamily.dylib"
/bin/cp "$root/VPhoneDaemon/Configuration/vphoned.plist" "$resources/guest/vphoned.plist"
/bin/cp "$guest_products/camfix/libcamfix.plist" "$resources/guest/libcamfix.plist"
/bin/cp "$guest_products/vcamcaptured/libvcamcaptured.plist" "$resources/guest/libvcamcaptured.plist"
/bin/cp "$daemon_products/vphoned" "$guest/vphoned"
/bin/cp "$root/VPhoneDaemon/Configuration/vphoned.plist" "$guest/vphoned.plist"
/bin/cp "$guest_products/launchhook/launchdhook-vphone.dylib" "$guest/launchdhook-vphone.dylib"
/bin/cp "$guest_products/systemhook/SystemHook-vphone.dylib" "$guest/SystemHook-vphone.dylib"
/bin/cp "$guest_products/camfix/libcamfix.dylib" "$guest/libcamfix.dylib"
/bin/cp "$guest_products/camfix/libcamfix.plist" "$guest/libcamfix.plist"
/bin/cp "$guest_products/vcamcaptured/libvcamcaptured.dylib" "$guest/libvcamcaptured.dylib"
/bin/cp "$guest_products/vcamcaptured/libvcamcaptured.plist" "$guest/libvcamcaptured.plist"
/bin/cp "$guest_products/gpu/libAppleParavirtCompilerPluginIOGPUFamily.dylib" \
"$guest/libAppleParavirtCompilerPluginIOGPUFamily.dylib"
"${0:a:h}/SyncStrings.sh"
for catalog in Localizable InfoPlist; do
@@ -82,7 +86,7 @@ compatibility_library="$(/usr/bin/xcrun swift-stdlib-tool --print \
/bin/rm -f "$bundle/Contents/Frameworks/libswiftCompatibilitySpan.dylib"
/usr/bin/codesign --force --sign - "$macos/vphone-cli"
/usr/bin/codesign --force --sign - --entitlements "$root/VPhoneDaemon/Configuration/VPhoneDaemon.entitlements" "$macos/vphoned.signed"
/usr/bin/codesign --force --sign - --entitlements "$root/VPhoneDaemon/Configuration/VPhoneDaemon.entitlements" "$guest/vphoned"
/usr/bin/codesign --force --sign - "$macos/VPhoneEscalator"
/usr/bin/codesign --force --sign - "$macos/libswiftCompatibilitySpan.vphone.dylib"
/usr/bin/codesign --force --sign - --entitlements "$root/VPhoneExecutable/VPhoneVirtualization/Resources/VPhoneVirtualization.entitlements" "$macos/vphone-vm"
@@ -6,6 +6,7 @@ root="$(cd "${0:a:h}/../../.." && pwd)"
bundle="${1:-$root/.build/XcodeBundle/Build/Products/Debug/VPhone.bundle}"
macos="$bundle/Contents/MacOS"
resources="$bundle/Contents/Resources"
guest="$resources/guest-resources"
file_copy_spawns="$(/usr/bin/find "$root/VPhoneExecutable" "$root/VPhoneKit" \
"$root/VPhoneDaemon" "$root/VPhoneGuestComponents" \
@@ -19,39 +20,40 @@ file_copy_spawns="$(/usr/bin/find "$root/VPhoneExecutable" "$root/VPhoneKit" \
[[ -d "$bundle" ]] || { print -u2 "Missing Xcode bundle: $bundle"; exit 1; }
for name in vphone-vm vphone-cli VPhoneEscalator vphoned.signed \
libswiftCompatibilitySpan.vphone.dylib libcamfix.dylib libvcamcaptured.dylib \
launchdhook-vphone.dylib SystemHook-vphone.dylib \
libAppleParavirtCompilerPluginIOGPUFamily.dylib; do
[[ -f "$macos/$name" ]] || { print -u2 "Missing binary: Contents/MacOS/$name"; exit 1; }
/usr/bin/file "$macos/$name" | /usr/bin/grep -q 'Mach-O' || {
print -u2 "Not a Mach-O: $name"
require_signed_macho() {
local file="$1"
[[ -f "$file" ]] || { print -u2 "Missing binary: ${file#$bundle/}"; exit 1; }
/usr/bin/file "$file" | /usr/bin/grep -q 'Mach-O' || {
print -u2 "Not a Mach-O: ${file#$bundle/}"
exit 1
}
/usr/bin/codesign --verify "$macos/$name" || {
print -u2 "Invalid signature: $name"
/usr/bin/codesign --verify "$file" || {
print -u2 "Invalid signature: ${file#$bundle/}"
exit 1
}
}
for name in vphone-vm vphone-cli VPhoneEscalator libswiftCompatibilitySpan.vphone.dylib; do
require_signed_macho "$macos/$name"
done
for name in vphoned launchdhook-vphone.dylib SystemHook-vphone.dylib libcamfix.dylib \
libvcamcaptured.dylib libAppleParavirtCompilerPluginIOGPUFamily.dylib; do
require_signed_macho "$guest/$name"
done
for name in vphoned.plist libcamfix.plist libvcamcaptured.plist; do
[[ -f "$guest/$name" ]] || { print -u2 "Missing guest configuration: $name"; exit 1; }
done
for name in vphoned vphone-app VPhoneAMFIAllow vphone-archive icli vpregister vphone-ask-for-permission; do
[[ ! -e "$macos/$name" ]] || { print -u2 "Obsolete binary: $name"; exit 1; }
for name in vphoned vphoned.signed vphone-app VPhoneAMFIAllow vphone-archive icli vpregister \
vphone-ask-for-permission libcamfix.dylib libvcamcaptured.dylib launchdhook-vphone.dylib \
SystemHook-vphone.dylib libAppleParavirtCompilerPluginIOGPUFamily.dylib; do
[[ ! -e "$macos/$name" ]] || { print -u2 "Obsolete binary: Contents/MacOS/$name"; exit 1; }
done
for name in guest scripts; do
[[ ! -e "$resources/$name" ]] || { print -u2 "Obsolete directory: Contents/Resources/$name"; exit 1; }
done
[[ -f "$resources/guest/vphoned.plist" ]] || {
print -u2 "Missing guest configuration: vphoned.plist"
exit 1
}
[[ ! -e "$resources/scripts" ]] || {
print -u2 "Obsolete scripts directory in bundle"
exit 1
}
/usr/bin/codesign --verify --strict "$bundle"
/usr/bin/codesign --verify "$macos/vphone-vm"
/usr/bin/codesign --verify "$macos/vphone-cli"
/usr/bin/codesign --verify "$macos/VPhoneEscalator"
/usr/bin/codesign --verify "$macos/vphoned.signed"
[[ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundlePackageType' "$bundle/Contents/Info.plist")" == "BNDL" ]] || {
print -u2 "The product is not a generic bundle"
@@ -61,11 +63,23 @@ if /usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$bundle/Contents/Info
print -u2 "The container must not declare an executable"
exit 1
fi
# Host programs live in Contents/MacOS and guest payloads in guest-resources.
# The build platform keeps an iOS binary from landing among the host programs.
while IFS= read -r file; do
if /usr/bin/file "$file" | /usr/bin/grep -q 'Mach-O' && [[ "$file" != "$macos/"* ]]; then
print -u2 "Mach-O outside Contents/MacOS: $file"
exit 1
fi
/usr/bin/file "$file" | /usr/bin/grep -q 'Mach-O' || continue
platform="$(/usr/bin/vtool -show-build "$file" 2>/dev/null | /usr/bin/awk '$1 == "platform" {print $2; exit}')"
case "$file" in
"$macos/"*)
[[ "$platform" != IOS ]] || { print -u2 "iOS binary in Contents/MacOS: ${file#$bundle/}"; exit 1; }
;;
"$guest/"*)
[[ "$platform" == IOS ]] || { print -u2 "Non-iOS binary in guest-resources: ${file#$bundle/}"; exit 1; }
;;
*)
print -u2 "Mach-O outside Contents/MacOS and guest-resources: ${file#$bundle/}"
exit 1
;;
esac
done < <(/usr/bin/find "$bundle/Contents" -type f)
bundle_entitlements="$(/usr/bin/codesign -d --entitlements - --xml "$bundle" 2>/dev/null || true)"
[[ "$bundle_entitlements" != *'com.apple.private.virtualization'* ]] || {
@@ -81,7 +95,7 @@ vm_entitlements="$(/usr/bin/codesign -d --entitlements - --xml "$macos/vphone-vm
print -u2 "Guest daemon entitlements leaked into vphone-vm"
exit 1
}
daemon_entitlements="$(/usr/bin/codesign -d --entitlements - --xml "$macos/vphoned.signed" 2>/dev/null)"
daemon_entitlements="$(/usr/bin/codesign -d --entitlements - --xml "$guest/vphoned" 2>/dev/null)"
[[ "$daemon_entitlements" == *'com.apple.CommCenter.fine-grained'* &&
"$daemon_entitlements" != *'com.apple.private.virtualization'* ]] || {
print -u2 "vphoned has the wrong entitlements"
@@ -20,6 +20,6 @@ Metal compilation, leaving the host VM window black even while vphoned connects.
`main.mm` is the compiler-plugin reimplementation from
[0xjohnnydev's metal-patch](https://github.com/0xjohnnydev/0xjohnnydev.github.io/blob/main/blog/assets/metal-patch/main.mm).
`make -C VPhoneGuestComponents gpu` builds and ad-hoc signs it for iPhoneOS arm64e.
The Xcode app build stages it in `Contents/MacOS`.
The Xcode bundle build stages it in `Contents/Resources/guest-resources`.
`fw prepare` copies that dylib alongside the
firmware-sourced GPU driver before exposing the complete restore tree.
@@ -98,9 +98,13 @@ public struct VPhoneResources: Sendable {
base.appendingPathComponent("scripts")
}
/// Files installed into the guest. Nothing here runs on the Mac.
public var guestResources: URL {
base.appendingPathComponent("guest-resources")
}
public var vphoned: URL {
let bundled = base.deletingLastPathComponent()
.appendingPathComponent("MacOS/vphoned.signed")
let bundled = guestResources.appendingPathComponent("vphoned")
if FileManager.default.fileExists(atPath: bundled.path) {
return bundled
}
@@ -109,7 +113,7 @@ public struct VPhoneResources: Sendable {
}
public var gpuCompilerPlugin: URL {
Self.siblingExecutable("libAppleParavirtCompilerPluginIOGPUFamily.dylib")
guestResources.appendingPathComponent("libAppleParavirtCompilerPluginIOGPUFamily.dylib")
}
// MARK: - Cache dirs
@@ -1,7 +1,8 @@
// VPhoneGuestBinaries.swift — where the prebuilt iOS binaries live.
//
// vphoned is cross-compiled on the build machine and shipped in the bundle.
// The bundle carries the signed binary for installation and host auto-update.
// Guest payloads are cross-compiled on the build machine and shipped in the
// bundle's guest resources. The bundle carries the signed vphoned for
// installation and host auto-update.
import Foundation
@@ -20,17 +21,18 @@ public enum VPhoneGuestBinaries {
}
}
/// `Contents/MacOS` in the bundle, `.build/guest` in a dev tree.
/// `Contents/Resources/guest-resources` in the bundle, `.build/guest` in a
/// dev tree.
///
/// Both are derived from `VPhoneResources.base`, which is already the
/// running image's own location rather than anything on `PATH` — so this
/// finds the binaries that were built beside this one, not whatever else is
/// on the machine.
public static func directories() -> [URL] {
let base = VPhoneResources.resolve().base
let resources = VPhoneResources.resolve()
return [
base.deletingLastPathComponent().appendingPathComponent("MacOS"),
base.appendingPathComponent(".build/guest"),
resources.guestResources,
resources.base.appendingPathComponent(".build/guest"),
]
}
@@ -18,6 +18,7 @@ struct ResourcesTests {
let exe = "/Applications/VPhone.bundle/Contents/MacOS/vphone-cli"
let r = VPhoneResources.resolve(executablePath: exe)
#expect(r.base.path == "/Applications/VPhone.bundle/Contents/Resources")
#expect(r.guestResources.path == "/Applications/VPhone.bundle/Contents/Resources/guest-resources")
}
@Test func `dev layout walks up to project root`() throws {