1.7.1 works around the Swift 6.4 defect that linked _swift_initBorrow
strongly (apple/swift-collections#739). A Release vphoned built with it
imports no _swift_initBorrow; StageBundle.sh keeps refusing one that does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bundle 2.2.5 (build 23) fixes iOS 18 guests that stopped at the Apple logo:
the camera hook typed the still sink's FourCC media type as an object, so
cameracaptured crashed on launch and SpringBoard waited on its flashlight
service forever (#541).
Launchpad 2.2.5 adds Install Skill to Host Setup with the vphone-guest-control
skill bundled inside the app, and the Core Bundle sheet no longer jumps when an
install starts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
swiftformat output only: wrapped single-line bodies, sorted imports, dropped
redundant self, throws and async, and plain numeric literals. No behavior
change; VPhoneCoreKitTests and the touched FirmwarePatcherTests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Host Setup gets an Install Skill button that opens a sheet with a copyable
prompt naming the skill folder inside the app, plus Show in Finder. The build
copies Skills/vphone-guest-control into Contents/Resources/Skills. The Core
Bundle sheet no longer shows an install-in-progress row, which made the layout
jump when it appeared.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The skill covers vphone-launchpad-cli, vphone.sock, vphoned methods, the
roothide and rootless layouts, the bootstrap and ssh flow, install problems
from the issue tracker, and where the research notes live.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
On iOS 18, -[BWStillImageSampleBufferSinkNode initWithInputMediaType:sinkID:]
takes the media type as a uint32_t ('vide'). The hook typed it as id, so ARC
retained 0x76696465 and cameracaptured crashed on every launch. SpringBoard
waits synchronously for cameracaptured's flashlight service at startup, so
the guest never left the Apple logo (#541).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bundle 2.2.4 (build 22) adds the tunnel network mode: the guest's traffic
leaves through ordinary connections opened by vphone-vm, so it follows a
VPN or proxy app on the Mac. vphone-vm and vphone-cli ignore SIGPIPE, so a
write to a closed socket no longer ends the VM. vphoned gains the three
accessibility client entitlements and an opt-in nested ui.tree that
follows the native parent-child links.
Launchpad 2.2.4 offers Tunnel in machine settings and New Machine, shows
it in the inspector, and leaves room for the Home button's glass bezel in
the title bar. Documents/Guides/networking.md describes the network modes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Machine settings and New Machine list Tunnel next to NAT, Bridged and
None, with a line saying what it does. The inspector showed a tunnel
machine's network as None; it now says Tunnel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The branch did not compile: the socket-pair initialiser delegated with
self.init without being a convenience initialiser, and three tests had
errors of their own (a shadowed binding, an argument out of order, and
self captured before the loopback server finished initialising).
Once built, four tests failed for reasons in the tests, not the stack:
two encoded the peer's MSS field where the SYN-ACK carries advertisedMSS,
one read its "before" count after a synchronous feed had already sent the
data it waited for, and one expected a frame to arrive with no known guest
MAC although the responder learns the MAC from that same frame.
On a real guest, vphone-vm died with signal 13 shortly after boot: a
forwarded TCP connection was written to after the server closed it. Every
tunnel socket now sets F_SETNOSIGPIPE, and vphone-vm and vphone-cli ignore
SIGPIPE for the whole process so a write to any vanished peer fails with
EPIPE instead of taking the guest down.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add Documents/Downloads/README.md listing which 2.x Launchpad releases
ship a notarized zip, and the rule that Launchpad x.y works with any
VPhone.bundle x.y.z. Record the same rule in AGENTS.md: patch releases
within a series stay interchangeable both ways.
Slim README.md: move the package environment steps into
Documents/Guides/package-environment.md and the project structure into
Documents/README.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The titlebar accessory clips to its frame, and the Liquid Glass bezel
grows a little past the button when pressed, so its leading edge was cut.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Launchpad 2.2.3. Starting an install from Core Bundle, or continuing from
Host Setup, closes the current sheet before the next one opens, instead of
showing the next sheet's content in the closing one. The strings that still
showed in English are translated, unused catalog entries are gone, and
Check for Updates comes before Install Local Build. Bundle 2.2.3 (build 21)
has no changes beyond its version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Removes the 81 catalog entries Xcode marked stale and no Swift source
still spells, most left behind by renamed or reworded UI. Five stale
entries whose text still appears in source stay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bundle install step "Install with administrator access", the Core
Bundle footer naming the bundle store, the CFW install step and its
failure message, and the Standard and Experimental preset descriptions
had only English. Adds ja, ko, vi and zh-Hans, reusing the catalog's
existing terms for the helper, custom firmware and patches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS, sheet(item:onDismiss:) calls onDismiss in the same update that
clears the item, while the sheet is still attached. Setting the queued
panel there swapped its content into the closing sheet, so starting an
install from Core Bundle showed the install view in the old sheet, faded
it out and presented it again. Setting it on the next turn of the main
actor lets the old sheet close with its own content first. Host Setup's
Continue took the same path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UDID override used to reach misagent and installd alone; Xcode, lockdown
and usbmuxd kept seeing the guest's own, so a paid team's profile could not
name the VM. The host reads the UDID in three places, and each is reachable
from userspace:
- lockdownd and remoted join vpIsMISFixTarget, so the spawn hooks insert
libmisfix into them. Only the MobileGestalt interpose acts there
(MISFixProcessOnlyNeedsIdentity keeps the MIS detours out). The hook now
matches the obfuscated key remoted asks with, re6Zb+zwFKJNlkQTUeT+/w.
- MGCopyAnswerWithError takes three arguments; the hook declared two and
crashed remoted, the first hooked caller of that spelling.
- vphoned sets the USB serial string, which is what usbmuxd names a device
by (vphoned_usb.m, com.apple.private.usbdevice.setdescription, with
AllowMultipleCreates), goes off the bus and back, and reapplies it at boot
once the USB device exists.
- udid.set/clear SIGKILL the hooked daemons (remoted ignores SIGTERM) and
always re-enumerate, which is also what relaunches remoted.
Measured on test-27.0: idevice_id, lockdown and the RSD handshake over both
transports report the override after udid.set and after a reboot, and the
guest's own after udid.clear.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Mac's CoreLocation fix has vertical accuracy -1 (no altitude). IcliKit
0.7.6 refused it with "vertical accuracy must be 0 or more metres", so
every location.set from Sync Host Location failed. 0.7.7 accepts -1 as an
unknown altitude.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
locationd never answers a client inside a generic bundle. From
VPhone.bundle/Contents/MacOS, vphone-vm's requestWhenInUseAuthorization
neither prompted nor changed the status, so Sync Host Location stayed at
Not Determined and sent nothing. Every .bundle layout behaved the same;
only an executable inside an .app was prompted.
vphone-vm now starts Contents/Helpers/VPhoneLocation.app (vphone-location,
an LSUIElement app) when sync is on. It asks for permission with its own
InfoPlist.strings, so the prompt text is localized, and writes one JSON
line per authorization change or fix. It exits when vphone-vm closes its
stdin. The bundle's own location keys and vphone-vm's location
entitlement are gone, ValidateBundle admits the helper and checks its
prompt translations, and the "Host Location Unavailable" alert is
localized.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bundle 2.2.1 (build 19). An app signed with a development certificate
launches on iOS 27.0: libmisfix now reaches SpringBoard, inserted at spawn
like installd's and misagent's, with no system binary modified for it.
Launchpad is unchanged: the app stays 2.2.0 and still accepts VPhone.bundle
2.2.0 or later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SpringBoard did carry SystemHook; what it lacked was libmisfix beside it.
Which libraries a process gets is decided in its parent, and launchd starts
SpringBoard itself, so SystemHook's list naming it was never consulted. A
probe reading KERN_PROCARGS2 showed DYLD_INSERT_LIBRARIES held SystemHook
alone in SpringBoard and both libraries in installd and misagent.
- vpIsMISFixTarget moves to InjectionEnvironment.h and the launchd hook
asks it too, inserting libmisfix only when the dylib exists.
- vpInsertHooks dropped the extra library when the environment already
named SystemHook; fixed, with make test-injection-environment.
- No guest binary carries a libmisfix load command. The three declarations
that added them are gone, cfw install puts each .bak back and removes
/mf, and inject-dylib --reclaim-source-version goes with its only caller.
- SystemHook's logs are world-writable: a root-created 0644 file silently
dropped every line from a mobile process, which is what made SpringBoard
look as though it never carried the hook.
Measured on test-26.4 and test-27.0, both recreated from local IPSWs: from
the first boot SpringBoard, installd and misagent carry both libraries, and
AirBuild installed through installd opens on both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A paid team's IPA installed on test-27.0 and was refused at launch with
0xE8008026: SpringBoard asks MIS itself, and it never carried the hook. The
spawn route SystemHook-vphone.c listed it under was never reached.
- system-springboard-cfw-launch_authorization links libmisfix into
SpringBoard with a weak load command, as installd and misagent are.
- SpringBoard's header has 16 spare bytes, so the command names a /mf
root alias and inject-dylib --reclaim-source-version drops
LC_SOURCE_VERSION to leave the re-signer room for its signature.
- MISFixInstallPolicy now runs in installd only.
Measured on test-27.0 after a cold boot: SpringBoard loads libmisfix, MIS
returns 0x0 for AirBuild, and it launches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`devicectl device install app` and `process launch` time out against the
guest. ideviceinstaller goes through installd the way Xcode does, which is
the path the install-gate hooks sit on; vphoned's apps.install re-signs and
places the bundle itself, so it says nothing about installd.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MICodeSigningVerifier carries allowAdhocSigning as a settable property and
installd never sets it, exactly like the MIS option. Forcing the getter makes
the real validation succeed and fill signingInfo, so the caller reads a
signing identifier instead of nil.
Forcing performValidationWithError: to return YES did not work and is gone:
the verifier had already bailed, so its caller refused on a nil identifier. A
refusal can be allowed through; an answer that was never computed cannot be
invented.
The class dump that found the property stays, but now runs only when a
selector this file expects has gone — the one moment it earns its length.
Measured on test-26.4: a codesign --sign - bundle with no certificate and no
profile installs through devicectl and launches, and so does a paid team's
dev-signed IPA.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An interpose never reached installd, so none of libmisfix's signature work
had ever run there. Replace it with a detour at the top of the callee, and
answer the two MobileInstallation refusals above it.
- MISFixDetour now takes an address, because no spelling of dlsym can
return one dyld has not interposed. It refuses a target shorter than the
four-word jump, which is what MISValidateSignatureAndCopyInfo is.
- MISFixProfileScope answers ProvisionsAllDevices for every profile, so the
embedded profile installs for real and MIS validates the app against it
with a genuine signer, entitlements and cdhash.
- MISFixInstallPolicy swizzles the embedded-profile install and the code
signing verifier, letting each refusal through after the real
implementation has run.
- MISFixNote appends to a file as well as the unified log, because a live
syslog tail has no lookback and every hook reports from a constructor.
Measured on test-26.4: a paid team's dev-signed IPA installs through
devicectl and launches. An ad-hoc signature is accepted by MIS and still
refused above it; Research/0_binary_patch_comparison.md says where.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Shift-Command-3 saves, Shift-Command-4 copies, as on the Mac. Neither
collides: Shift-Command-C already opens the guest clipboard.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
vphoned saw nothing through Security.framework, so every row the browser
showed came from the keychain database as encrypted metadata, and the
only delete path was the test item's account and service pair.
Give vphoned the "*" keychain access group, which securityd special
cases into every group, and name items by class, account, service,
server and group. An identity with no attribute at all is refused so a
query cannot widen into a whole class. keychain.get and keychain.update
now belong to GuestKeychain alone; the file tool copies passed "genp"
straight to secClass(named:), which never accepted it.
Accessible rows list as hidden rather than protected: the value is one
read away, not out of reach. The strings file also names the trackpad
menu item, which had no entry at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Reword user-facing errors...` (090df08) and `Rewrite error messages and
labels...` (c27ea64) replaced the wording the Python bridge used with text
that says what failed and what to do next. Three tests still asserted the
old strings, so VPhoneRestoreTests has been failing ever since on five
expectations that describe a message nothing produces.
Assert the current messages and rename the tests, which no longer claim to
pin Python's wording. The type's doc comment made the same stale claim; it
now says the cases came from the bridge and the messages did not.
VPhoneRestoreTests: 67 tests in 6 suites pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured on test-26.4. Copy-on-write on a shared-cache text page works: the
page splits out as its own region and comes back prot=7, max=7. The store
then faults anyway —
EXC_BAD_ACCESS (SIGBUS), UNKNOWN_0x32 at 0x1027543dc
__TEXT 102754000-102758000 [16K] rwx/rwx SM=COW /usr/lib/libmisfix.dylib
because Apple silicon enforces write-xor-execute below the VM permissions.
So RWX is not the safe request, it is the broken one, and the first run's
RW-without-X was only wrong because the page it dropped execute from was the
page the probe was executing. Both spellings crash, for opposite reasons,
and they look identical from outside; both are written down where the next
person will look.
The probe now drops execute for the write and the own-text warm-up is gone,
since that page is the one that must keep it. The executable-memory step
tries write-then-mprotect before RWX, same reason.
MISFixDetour is the fix this was all for: four words at the top of the
callee become an absolute jump, the displaced instructions move to a
trampoline, and every caller is redirected wherever it lives — which is the
whole point, since installd's callers are inside the cache. adr/adrp are
rewritten to materialise the same address and an unconditional b becomes an
absolute jump; bl, the conditional branches and literal loads are refused
rather than guessed at, because a wrong relocation is a corrupted daemon and
a refusal is a log line. Every emitted word was checked against the
assembler, not remembered.
The in-image trampoline arena is deliberately absent: filling it means
dropping execute from a page of __TEXT whose other occupant is the code
doing the dropping.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Row 17's note claimed libmisfix "covers installation". It does not, and the
paragraph is superseded rather than edited, so the reasoning that was wrong
stays readable next to what replaced it.
The capture, the three corrections it forces — the signature was never the
problem, libmis resolves a UDID the interpose never sees, AllowAdHocSigning
has never taken effect in installd — and the three routes still open, with
why a detour is preferred over another libmis cache patch after #532.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SystemHook inserts this dylib into misagent and SpringBoard as well, and the
executable-memory step can end the process outright rather than return an
error. In SpringBoard that is a respring, repeating for as long as the flag
is on. installd is on-demand, so launchd starts it again for the next client
and a kill there costs one failed install.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rewriting a callee needs two things the guest may or may not allow, and one
capture should settle both rather than costing a deploy each.
Writing the page is now measured twice: this dylib's own text first, which
is private, file-backed and already carries write in its maximum protection,
then the cache text the detour actually targets. The easy case failing would
mean nothing further is worth reading, and it is also a candidate home for
trampolines, so it is measured rather than assumed.
Executable memory is the other half, and the reason it comes last: a
trampoline is memory this process fills in and then jumps to, which is what
codesigning exists to stop, and a page the kernel has not blessed is a kill
rather than an error return. Both spellings are tried — PROT_EXEC straight
from mmap, then write plus mprotect, which is what a process without dynamic
codesigning has to do and the one likelier to survive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
vphoned reports `setup_pending` in /v1/health and serves `setup.status`
and `setup.skip`. Device › Skip Setup Assistant… is enabled while the
guest reports it pending, and asks before it runs.
SpringBoard decides whether to run Setup once, when it starts. It does
while `SetupDone` in com.apple.purplebuddy is not true, and runs the flow
shown after a software update while `SetupVersion` is below
SetupAssistant.framework's BYBuddyIOSCurrentVersion (an int32, 11 on 26.4
and 27.0). Writing the keys while Setup is on screen does not dismiss it,
and killing Setup only makes SpringBoard start it again. So the skip
writes `SetupDone`, `SetupFinishedAllSteps` and `SetupVersion` through
cfprefsd for user mobile, then restarts SpringBoard. With every other key
in the domain removed, those three still reach the Home Screen, and no
later panes appear.
The experiments are in Research/Guest/setup_assistant_skip.md, including
how to send a guest back to Setup for testing. MCInstall's
SetCloudConfiguration, which pymobiledevice3, go-ios and cfgutil use,
works only on an erased device, so it does not fit here.
Verified on test-26.4 with this bundle: after deleting `SetupDone`,
Setup's hello screen appeared and setup.status reported pending true and
running true. setup.skip without force was refused. With force it
returned pending false, SpringBoard restarted and unlocked to the Home
Screen. Deleting `SetupVersion` then skipping also reached the Home
Screen. Afterwards the domain matched its state before the test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The measurement is in: an interpose does not reach a call made from one
shared-cache image to another. installd's whole install produced one line,
`MGCopyAnswer(BuildVersion) from installd` — the main executable's own call
and nothing else. No UniqueDeviceID query, although libmis resolved one; it
skipped every installed profile with 0xE8008012 and returned 0xE8008015.
No MISValidateSignatureAndCopyInfo line either, from a log that is now
unconditional. The signature was never the problem: `cdhash is trusted`.
So MISFixSignature.c's premise was wrong — installd does not decline to ask
MIS for ad-hoc acceptance, it never reaches this hook at all — and the
UniqueDeviceID key does not make an Xcode install succeed by itself. Both
files now say so, and say where the remaining fixes live: a shared-cache
patch, or a VM created with the ECID of an already-registered device, since
a modern UDID is <chip-id>-<ECID> and only the ECID is chosen.
Rewriting the callee instead of the call sites would reach every caller.
That costs a trampoline and arm64e relocation work, all of it wasted unless
the process can make a cache text page writable, so the probe asks that one
question behind its own flag and writes back the bytes already there.
Its first run got both halves wrong in a way worth keeping written down.
dyld applies interposing to dlsym, so RTLD_DEFAULT returned this dylib's own
replacement; and asking for write without execute on the page then executing
from it faulted immediately, which crash-looped installd until the flag went
off. The symbol is now resolved through a handle on libMobileGestalt, the
target is refused if it lands in this image, and the request is RWX.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first LogQueries run left the question half open. installd loaded the
hook and logged MGCopyAnswer(BuildVersion), then never logged a
UniqueDeviceID query — yet libmis plainly resolved one, because it skipped
every installed profile with 0xE8008012 before returning 0xE8008015. Two
incompatible readings fit that: installd's own code asked for the build
version and the frameworks ask past a __DATA,__interpose replacement, or the
interpose does reach cache-to-cache calls and libmis finds the UDID
somewhere other than MobileGestalt. They call for opposite fixes.
MISFixCallerImage resolves a return address to its image, so each line says
who asked. installd means the main executable and an interpose that stops at
the cache boundary; MobileInstallation or libmis means it crosses.
The validation log was ambiguous in its own way: it returned early when the
path would not convert, which is indistinguishable from never being called.
It now always logs, and says what the argument was instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`cfw install` needs a prepared restore tree, and the restore tree is
deleted once a VM has first booted. So an existing machine could not be
moved onto a newer bundle at all: a new vphoned or a new guest dylib had
nowhere to come from, and the machine had to be rebuilt.
Replacing the files in a running guest over the API does not solve it
either, and not only for permissions: a daemon keeps the copy of a
library it mapped at launch, so writing /usr/lib/libmisfix.dylib under a
live installd changes nothing until installd restarts.
So the installer gains a mode. `.environmentOnly` mounts Disk.img
through the same clone-and-swap path as a full install and puts back
only the files a full install writes into the guest — vphoned, its
launch daemon, the guest dylibs, and the libmisfix defaults if the VM
has none. It runs no shared-cache or Mach-O patch, injects no load
command, does no cryptex or GPU work, skips the Preboot patches and the
snapshot rename, and leaves the recorded variant alone.
Two refusals keep it honest. It needs no restore tree, but it does need
`launchd.plist.bak`, which only the first `installVphoned` writes: with
nothing to put files back *over*, this would be laying down half an
install, so a VM that was never installed is refused. And a library the
guest does not already have is left out rather than added, because its
absence means the VM's plan never selected it.
The files it does write go through the same confined descriptors, modes
and owners as the full install, by calling the same `installVphoned` and
`installMISFixDefaults`. `installVphoned` rebuilds launchd.plist from
the backup rather than editing it, so the daemon cannot be injected
twice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A machine's restore tree is removed after its first boot, so cfw install
cannot run again and a machine made by an older bundle never gets newer
hook dylibs. The helper gains updateGuestEnvironment, which runs the
bundle's `vphone-cli cfw update-environment` as root through the same
checks, output channel, lock and cancel as the install. Launchpad offers
Update Guest Environment in a stopped, installed machine's menu, and
vphone-launchpad-cli gains `cfw update-environment <name>`.
The helper protocol changed, so CURRENT_PROJECT_VERSION goes to 9 and
Launchpad reinstalls the helper. The CLI verb itself lands with the
installer's resource-only mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
libmisfix.dylib was already in the environment update's list on both
sides, but replacing the file changed nothing until something restarted
the daemons holding the old copy mapped, the same way the camera daemon
already needed restarting for libvcamcaptured. A new SystemHook counts
too, since that is what inserts libmisfix into them.
SpringBoard is deliberately not restarted. That is a respring, which is
`system.respring` to ask for, not something an environment update should
do behind the back of whoever is looking at the screen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The earlier diagnostic could log MobileGestalt queries, which is half of
what the installd question needs. "installd logged nothing" has two
readings — the call never came through the interposed symbol, or the
hook was not in the process at all — and they call for opposite fixes.
So the dylib now announces itself from a constructor, naming the
executable it landed in, and MISValidateSignatureAndCopyInfo logs each
validation and its result. That second one is the control that matters:
it is reached exactly the way the UDID query is, MobileInstallation to
libmis, both in the shared cache, with the main executable's image not
involved. If the validation line appears in installd and the
MGCopyAnswer line does not, the two calls are being treated differently
and the difference is in MobileGestalt rather than in whether an
interpose can cross the cache at all.
All of it is behind the existing LogQueries flag, off by default, and
goes through one MISFixLog with a single prefix so one predicate finds
every line whichever process is carrying the hook.
The config cache holds the whole dictionary now rather than just the
UDID, so the flag costs no second read and every value stays consistent
with the file it came from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
libmisfix reached installd and misagent as an LC_LOAD_WEAK_DYLIB that
cfw install wrote into each binary. SystemHook already interposes
posix_spawn and already decides per path what a child gets, so it owns
this now: installd, misagent and SpringBoard get libmisfix added to
DYLD_INSERT_LIBRARIES, and nothing else does.
Linked versus inserted is not a detail here, and is probably the bug.
A weak load command makes the hook a dependency of the main executable,
which is enough to interpose the calls that executable makes itself —
misagent asks MobileGestalt for the UDID directly, and its override has
always worked. It is not enough for a call made between two shared-cache
images, and installd's check is exactly that: +[MICodeSigningVerifier
_validateSignatureAndCopyInfoForURL:withOptions:error:] lives in
MobileInstallation and calls libmis, with installd's own image not
involved. That call kept seeing the guest's real UDID and refusing the
app with 0xE8008015 even after a fresh installd. DYLD_INSERT_LIBRARIES
loads the hook ahead of everything else, which is where an interpose
covers the cache's own uses of a symbol too.
SpringBoard is in the list for the gap dropping the cache patch left
open: it asks MIS again at launch, so an app signed with a free
personal-team certificate could be installed and then refused with
0xE8008026. It already received SystemHook, because vpIsAppPath matches
any path containing ".app/"; it just never received this hook.
Targets are matched on the end of the path, so a bootstrap or cryptex
copy of the same binary is caught too. vpInsertHook keeps its signature
and forwards to vpInsertHooks, so the launchd hook is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On test-26.4 the UDID override reaches misagent — a paid-team profile
whose ProvisionedDevices lists the override installs, and one that does
not is refused with 0xE8008012, exactly as it should be. installd then
refuses the same app with 0xE8008015, "A valid provisioning profile for
this executable was not found", and a hand-restarted installd does it
again, so it is not staleness.
Two explanations fit that equally well from outside: installd asks and
gets the wrong answer, or installd never asks through this symbol at
all. They are not distinguishable without instrumenting, and the second
is quite likely — `MICodeSigningVerifier` lives in MobileInstallation,
not in installd, and it calls libmis, so the query that matters is made
cache-to-cache between two shared-cache images rather than from the main
executable the way misagent's is. installd's own imports are only
_MGCopyAnswer and _MGGetBoolAnswer, the same as misagent's, so the
import table cannot tell them apart either. The one MobileGestalt line
the guest logged during a failing install was from installd and said
"elided platform fast path for key: re6Zb+zwFKJNlkQTUeT+/w".
So the hook now logs each query and whether it answered, behind a new
`LogQueries` flag that is off by default — these daemons are asked a
lot, and the log is how a person watches an install. If an install
produces no line from installd, the call is not coming through
MGCopyAnswer and the hook needs a different point to stand on.
The config cache now holds the whole dictionary rather than just the
UDID, so a second setting costs no second read and every value stays
consistent with the file it came from. MISFixCopyConfiguredDeviceIdentifier
returns the dictionary's own string instead of a copy; it was already
documented as borrowed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
IcliKit 0.7.5 counted AppleDouble sidecars as bundles: an IPA made with
Finder's Compress or ditto without --norsrc carries Payload/._Name.app,
and apps.install refused it with "IPA must contain exactly one
Payload/*.app". 0.7.6 skips __MACOSX and ._* files during extraction
and in the container bundle lookup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
udid.set restarted misagent and deliberately left installd alone, on the
grounds that installd "asks misagent". It does not. A lockdown-path
install runs +[MICodeSigningVerifier
_validateSignatureAndCopyInfoForURL:withOptions:error:], which calls
MISValidateSignatureAndCopyInfo and evaluates the profile's
ProvisionedDevices itself. Two processes answer the UDID question
independently, which is why cfw install injects libmisfix into both, so
refreshing one and not the other leaves them disagreeing.
Measured on test-26.4: with the override changed to a UDID that is in
the profile and only misagent restarted,
_installEmbeddedProfilesWithError: passed and installd then failed the
same install with 0xE8008015, still holding the UDID from before the
change. installd had been up since boot (pid 281) while misagent had
been restarted 24 minutes later (pid 819).
Restarting installd can abort an install already in flight. That is the
lesser evil: changing the device's identity underneath a running install
is incoherent anyway, whereas leaving one of two evaluators on a stale
answer fails later, somewhere else, with an error that never mentions
the UDID.
This is necessary but, on its own, not sufficient — a hand-restarted
installd still refuses the same install. The remaining half is that
installd's UDID query may not reach this hook at all; see the next
commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The config search picked the first path that `stat`ed, then read it and,
if the read failed, reported no override. So the /usr/lib copy only ever
acted as a fallback when `stat` itself failed.
Its whole documented purpose is the case where that is not true. The
header says it is "the fallback if a daemon's sandbox turns out not to
reach /var/db", and a sandbox that allows metadata while denying read
leaves `stat` succeeding and `open` failing — which selected
/var/db/vphone/misfix.plist, read nothing from it, and returned no
override. That is the one result indistinguishable from the hook working
correctly and finding nothing configured, so the failure it was built to
survive was also the failure it could not report.
A candidate is now adopted only when it reads and parses; anything else
falls through to the next. A file that parses but sets no UniqueDeviceID
still counts as adopted, because an explicitly present, valid, empty
configuration means "no override" rather than "keep looking".
The mtime+size cache is kept and still costs one `stat` on the common
path — misagent asks once per profile and installd once per bundle — by
re-checking the file already chosen before searching again. Reading every
candidate on every query would have been the obvious way to write this
and would have reparsed the plist on every MIS call.
Found while verifying the override end to end on test-26.4, where
/usr/lib/libmisfix.plist is an empty dict and /var/db/vphone/misfix.plist
carries the UDID, so the two paths give different answers and picking
the wrong one is silent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two findings from the pristine 24A435 cache, neither of which changes
what `standard` does — it still blocks this patch.
MIS has not been rewritten; the error message said so and was wrong.
24A435's libmis never materialises 0xE8008026 at all: a whole-image
decode of 94,984 instructions finds no mov-family instruction with
immediate 0x8026 and no such word in the data. It seeds the base
0xE8008001 and *adds* its way up (add w26, w23, #0x25), where 26.6.2
seeded 0xE8008026 and subtracted down. Everything else matched: the
naming literal occurs once, has exactly one adrp+add reference, that
reference is inside the function, and the nearest preceding pacibsp is
the function start. Only findSeededError missed.
It now accepts both, and they are not interchangeable. 0x8026 stands on
its own. 0x8001 is only the bottom of the MIS error range, so it is
accepted only when the same function also holds an add whose result is
arithmetically 0xE8008026, and the scan stops at the next function's
pacibsp. That corroboration is load-bearing: the function preceding
checkTrustAndAuthorization carries the identical seed idiom 18
instructions earlier, which is also why the seed window is forward-only.
Matching is on decoded immediates and registers; nothing new is written,
so there is no new encoder and no keystone trip.
The declaration also gets applicability .oneOf([.major(18), .major(26)]).
experimental is Kind=All, so without it a 27 user would still have the
patch turned on — and after the matcher fix it would now succeed in
bricking their guest rather than failing safe. This is not a preference,
which would belong in a preset's block list; it is the statement
applicability exists for, that applying it there breaks the guest. An
unreadable base satisfies only .any and so skips the patch.
Verified on-device: with the patch out of standard, cfw install
test-27.0 completes and 24A435 + cloudOS 26.4 boots clean — panicked
false, vphoned in 6s, SpringBoard up, 264 apps. Issue #532 is closed and
its cause is confirmed to have been this patch.
Tests: 4 new, 438 total, same 132 pre-existing fixture issues. The
fixture's 32-bit add is derived from the keystone-checked encodeAddImm12
by clearing sf and asserted against Capstone, the same way its movk
already was.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README asked for apt and bash only. List the five packages to select
together on the first Bootstrap Install (apt, bash, uikittools,
launchctl, openssh-server), explain that openssh-server declares
circular or imprecise dependencies so one-by-one installs can fail, and
recommend removing and reinstalling the environment instead of repairing
a failed first install. Updated in all four READMEs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`cfw install` fails outright on a pristine 24A435 cache:
Patch site not found: checkTrustAndAuthorization: the prologue at
0x22406F814 neither seeds 0xE8008026 nor already reads
`pacibsp ; mov x0, #0 ; retab` — MIS has been rewritten
The cache really is pristine — the same run reports a first-time
`maxSlide 0x20000000 -> 0x0` and fresh lsd, libxpc and lockdown-mode
writes — so this is not the already-patched anchor that was fixed for
26.x. 24A435's checkTrustAndAuthorization matches neither shape.
Teaching the patcher the new shape would be the wrong fix. Even where it
applies, this patch stops an iOS 27 guest booting (issue #532): TXM
rejects the re-attested page, dyld cannot map libSystem, and initproc
never starts. Making it apply on 24A435 turns a failed install into a
guest that installs and then does not boot.
libmisfix.dylib already does the same job from userspace, and by the
better route — it steers the call instead of forging the return.
`vpWidenedOptions` passes `RespectUppTrustAndAuthorization = false`, and
libmis calls checkTrustAndAuthorization only when that flag is set, so
0xE8008026 is never produced and the success path still fills `info`
with the CdHash and entitlements its callers read. Nothing is written to
the cache, so there is nothing for TXM to reject. `cfw install` injects
it into installd and misagent.
So `standard` blocks the patch, and it is renamed dyld-cfw-mis_trust_auth
-> dyld-exp-mis_trust_auth as the naming rule requires for a patch the
standard preset leaves off. The patcher and the `cfw patch-mis-trust-auth`
verb are unchanged and still work when it is ticked on; `experimental`
is Kind=All and still turns it on.
The gap this leaves: libmisfix rides in installd and misagent, so the
hook covers installation, while an app signed with a free personal-team
certificate is launched by SpringBoard, which asks MIS itself. On a 26.x
base that launch can still hit 0xE8008026. Ad-hoc and ldid-signed apps
are unaffected — they carry no profile, so the online-authorization
branch is never reached — and paid teams never were. Closing it means
injecting the hook into SpringBoard, which needs SystemHook's posix_spawn
route rather than a load command.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pipeline patched each boot-chain component in place: load the file,
run the patchers, save over the same path. A second run therefore handed
the patchers the first run's output, they found none of the shapes they
had already replaced, and the component died with `Patch site not found:
iBSS`. `fw prepare` refuses to re-extract over an existing restore tree,
so there was no way back either — a VM could be patched exactly once, for
its whole life, and turning a patch off in its PatchSelection.plist could
not change anything on disk.
The first run now copies each component into <vm>/FirmwareOriginals/,
mirroring its path, before anything is written, and every later run
patches those bytes. The shipped container also goes back immediately
before `loader.save`, because ContainerFirmwareLoader repackages whatever
IM4P it finds at the destination and would otherwise wrap the second
run's payload in the first run's container. Same VM and same plan now
produce the same file however many times fw patch runs.
A plan that selects nothing for a component — every patch blocked, or the
whole set dropped so no patcher is built — restores the unpatched image
instead of leaving the last run's patches stranded with nothing selecting
them. A component nobody ever patched is not rewritten, so it keeps the
modification date the restore gave it.
Filesystem and Manifest opt out. Both name BuildManifest.plist, but
neither is a patcher over that one file: one rewrites cryptex images
across the restore tree, the other rewrites hashes describing files other
steps produced, so putting the manifest back alone would describe a tree
that no longer exists. `.less` is excluded outright, so a `.less` run over
a patched VM cannot read "this variant builds no boot-chain patchers" as
"put the boot chain back".
A VM patched by an older build has no originals, so the first run would
otherwise adopt its already-patched bytes as pristine. If the component
then fails to patch, the copy is deleted and the error says to remove the
restore tree and run fw prepare again. fw prepare also deletes a stash
left over from previous firmware — AVPBooter's file name carries no
version, so a stale copy would be silently reused — and a slim export
leaves the originals out along with the restore tree they describe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
vm launch on a VM whose CFW install did not finish printed the boot usage
after the reason, as ValidationError does, and told the user to stop a VM
that could not be running. The two bundle-state refusals now throw a plain
error. Found testing 2.2.0 through vphone-launchpad-cli.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
libmisfix answers misagent's UniqueDeviceID from /var/db/vphone/misfix.plist
(then /usr/lib/libmisfix.plist) and rereads it when it changes. vphoned now
serves udid.get, udid.set and udid.clear (capability udid_override): set
writes the data-volume plist atomically, keeping its other keys, reads it
back the way the hook resolves it, and sends SIGTERM to misagent so launchd
starts it fresh. installd is left alone and the guest is not rebooted. clear
removes the key but keeps the file, so a stale /usr/lib value cannot win.
The VM window's Device menu gets Set UDID… (prefilled, checks the 8-16 or
40 hex digit forms) and Reset UDID, enabled when the guest reports the
capability. Only misagent's profile check sees the value; Xcode, devicectl
and lockdown still see the guest's own UDID.
Also bumps Launchpad to 2.2.0.
Not yet verified on a running guest: that misagent's sandbox can read the
data-volume plist, and that launchd restarts misagent on the next check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Issue #531's black screen came from a guest whose cache was never patched:
its first `cfw install` had not completed. Nothing said so; launchd just
panicked at pid 1. `restore` writes restore-info.json without a variant and
only a finished install records one, so that file already tells the two
apart. `vm launch` now refuses such a VM and names the command to run, and
`vm list` reports `customFirmwareInstalled`. An install that has to write
Disk.img in place (no APFS clone) clears the variant first, so one that
stops halfway is not reported as complete. Bundles from before
restore-info.json existed read as unknown and still launch.
Launchpad shows "Custom Firmware Not Installed" in the inspector, refuses
Start with an explanation instead of logging a black screen, and offers
Install Custom Firmware in the machine menu. It runs through the helper and
writes to the machine's console log.
Core Bundle install progress moves out of the inspector into a sheet of its
own. It opens when an install starts, reopens on launch if the last one did
not finish, and can be hidden while it runs; Core Bundle offers Show
Progress until it ends.
Also localizes the new strings and the firmware names for ja, ko, vi and
zh-Hans, renames the stale Extended preset string to Experimental, fixes a
RestoreInfo test still expecting the old "Only JB VMs" message, and carries
swiftformat's pass over the MISFix tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every Jailbreak* type, file and directory in the firmware patcher is now
CustomFirmware*: Kernel/JailbreakPatches -> Kernel/CustomFirmwarePatches,
KernelJailbreakPatch* -> KernelCustomFirmwarePatch*, IBootJailbreakPatcher,
KernelJailbreakPatcher(Base), FirmwareKernelJailbreakPatchSet. The kernel
patch set is com.vphone.patchset.kernel.cfw and provides
vphone.kernel.cfw; the patcher's gate component is kernelcache_cfw; the
`fw patch-component` verb is kernel-cfw; log lines print [CFW].
The `extended` preset is now `experimental`, which is what it has always
been: every patch the bundle declares, including ones a freshly restored
guest may not survive. Launchpad's machine inspector stops showing the raw
restore variant and names the firmware instead — Standard Custom Firmware,
Experimental Custom Firmware, or Unknown Firmware for anything else.
Left alone deliberately: /var/jb and .jbroot-<hex> are rootless and
roothide's naming, not ours; vphoned's `jailbreak` API object and the
device-info panel that renders it describe that same environment; and
FirmwarePipeline.Variant.jb keeps its spelling because its raw value is
recorded in every existing VM's RestoreInfo, which is why the inspector
maps it rather than rewriting it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rows 18 and 19 of the CFW installation table cover
system-installd-cfw-adhoc_signature and
system-misagent-cfw-device_identity: what each one measures, why the
option keys are CFStrings rather than exported symbols, why the first
argument to MISValidateSignatureAndCopyInfo is a path and not a URL,
and the three cheaper routes to a settable UDID that were ruled out
before the MGCopyAnswer interpose was written.
Both rows say plainly what is and is not proven. The injection is
validated on 06-xcode-27: the guest boots to the home screen and both
daemons run with the dylib loaded. The installs themselves are not,
because devicectl will not complete a session against that guest on
this host.
Row 17's declaration name follows the rename to dyld-cfw-mis_trust_auth.
The rest of the document still spells several identifiers the old way;
that sweep belongs with the rename, not here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 117 bundled patch identifiers had grown five naming schemes
(kernel.x, jb.x, kernelcache_jb.x, txm_dev.x, bare names). Each one is now
{component}-{effect}-{name}:
- component: avpbooter, ibss, ibec, llb, txm, kernel, devicetree, dyld,
preboot, or system-<binary> for a guest binary or file.
- effect: boot when the patch is boot-essential, exp when the standard
preset leaves it off, cfw otherwise. A catalog test enforces this.
- name: snake_case, no hyphen, so the identifier splits from the right.
Record sites are now always <identifier>.<site>. The underscore-prefix
rule in covers(recordIdentifier:) and in the gate is gone: the new names
contain underscores, so kernel-boot-post_validation would otherwise have
covered kernel-boot-post_validation_unsigned. The 25 records that relied
on it (amfi_trustcache_1, launch_constraints_mov, sandbox_ext_N, ...) now
use a dot.
Old identifiers are not migrated. A VM whose PatchPlan or PatchSelection
names one must be patched again. The bundle becomes 2.2.0 and Launchpad
requires 2.2.0, so it never meets an old identifier from a bundle.
Launchpad's patch table shows Component, Effect and Name columns in place
of Identifier and Patch Set; the set moves to the detail line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Xcode could not install anything onto a guest unless it was signed with an
Apple leaf certificate, even though the guest runs unsigned code perfectly
well. Measured on a booted 26.6.2 guest: unsigned and ldid-shaped bundles
fail at 0xE800801C, a `codesign --sign -` bundle at 0xE8008014, all from
+[MICodeSigningVerifier _validateSignatureAndCopyInfoForURL:withOptions:error:].
A bundle pushed in through vphoned's apps.install, signed with nothing but
this project's own signature, installs and reaches the foreground — so the
kernel, lsd and SpringBoard already accept it and installd's check is the
only gate.
libmis accepts an ad-hoc signature outright when the caller passes the
AllowAdHocSigning option, which installd never does, and it fills the whole
info dictionary itself. So libmisfix.dylib interposes
MISValidateSignatureAndCopyInfo and adds the option, and cfw install attaches
it to installd with an LC_LOAD_WEAK_DYLIB. Nothing in the dyld shared cache
is touched, deliberately: writing a cache code page is what leaves a 27.0
guest unable to boot in #532.
The same dylib answers the other refusal. A paid team's profile fails at
0xE8008012 because the VM's UDID is in nobody's ProvisionedDevices, and only
a free personal team gets auto-registration. misagent obtains that UDID from
MGCopyAnswer -- its other source, an emulated UDID in the kernel's
codesigning configuration, is unreachable here: the sysctl is a four-byte
flags word and amfi_emulate_device_udid is in neither the kernelcache nor
TXM. Interposing MGCopyAnswer lets libmisfix.plist name a device the team has
already registered. Off until a UDID is set there.
What Xcode and lockdown report is unchanged and still the guest's own UDID;
TXM builds that one from the device tree before the kernel runs. The two
answers disagree on purpose, because agreeing would mean a re-restore for a
UDID that still could not match a real device's.
Also fixes#532's second defect: DyldSharedCacheMISTrustAuthPatcher now
recognises its own output, so a second cfw install reports alreadyPatched
instead of aborting the install.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The flag could not do anything any more. `dsc_maxslide.zero` is pinned to
iOS 27, and on a 27 base the installer's force branch was never reached:
it sat below the `27.` and `26.0`/`18.` cases. Issue #531 credited it with
a fix it could not have made.
The self-gate needs no force on 27. The pristine 24A435 cache reads
size 0x17D504000 + maxSlide 0x20000000 = 0x19D504000, over the 6 GiB
region, and `patch-dsc-maxslide --dry-run` reports overflow. XNU's
shared_region_map_and_slide_2_np picks a 16 KiB-aligned slide below
maxSlide and maps each range FIXED in the 0x180000000 submap, so
size + maxSlide <= region is a sufficient test.
Removed from vm create, restore, cfw install and install-root, the
create options, the Launchpad new-machine sheet and control verbs, and
the helper's installCustomFirmware XPC signature. A helper built before
this has a different hash and shows as outdated, so Launchpad reinstalls
it first. `patch-dsc-maxslide --force` stays on the standalone verb.
Docs: troubleshooting no longer offers the long-gone --force-exc-guard,
and FORCE_DSC_MAXSLIDE is gone from the patcher, verb help, research
notes and the patch-set skill. Row 10 of the patch comparison records
the 24A435 numbers and the source check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`input.touch2` accepted a `normalized` parameter and never read it, while
`input.touch` beside it honours one. `vp_hid_touch2` takes 0..1 only, so a
caller passing screen points the way `input.touch` allows would have had both
fingers read as fractions and land in the corner, silently. Refuse the flag
instead.
The view's `normalizeCoordinate(allowOutside:)` had reimplemented
`VPhoneDisplayGeometry.normalizedPoint` minus the clamp, leaving the mapping
in two places while only one of them is tested. The geometry type takes a
`clamped` parameter now, with a test for the path the scroll gesture uses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bundle 2.1.7 (build 18). vphoned now installs IPAs and reads debs whose
entries have UTF-8 names, through icli 0.7.5 (#530), and vphone-cli's
archive commands read and write such names too. Launchpad is unchanged:
the app stays 2.1.2 and still accepts VPhone.bundle 2.1.0 or later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vphoned runs in the C locale launchd gives it, and icli's archive readers
converted entry names through that locale. An IPA whose ZIP entries carry
the UTF-8 flag, such as Payload/App.app/What’s New.html, failed apps.install
with "Pathname cannot be converted from UTF-8 to current locale", and a
deb with a PAX UTF-8 path failed the same way. icli 0.7.5 gives each
archive reader a thread-local UTF-8 LC_CTYPE, so vphoned needs no locale
handling of its own.
Reported with a vphoned-side fix and regression harness by Yanni Pang in
#530; the fix moved into icli, which carries the ported tests.
Co-Authored-By: Yanni Pang <2459177+yannip1234@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
libarchive converts member names and link targets through the calling
thread's LC_CTYPE, and nothing in vphone calls setlocale, so vphone-cli
runs in "C" from a terminal, under Launchpad and under its root helper
alike. A zip entry with the UTF-8 flag came back with a NULL pathname,
which readMember reported as a missing member and extract turned into
"", resolving to the destination itself; a pax archive with a non-ASCII
name could not be created at all ("Can't translate pathname").
VPhoneArchiveReader, VPhoneArchiveExtractor and VPhoneArchiveWriter now
run each libarchive session inside withArchiveLocale, which gives the
calling thread a UTF-8 LC_CTYPE with uselocale and restores it after.
The extractor also refuses an entry whose name is still NULL. gnutar
exports never converted names and are unchanged. The same bug in the
guest's IPA and deb readers is fixed in icli 0.7.5 (#530).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bundle 2.1.6 (build 17). It carries the MIS trust/authorization patch, so
an app signed with a free personal-team certificate now launches on a
guest this project restored, and the macOS 27 CFW disk selection and
debugserver seatbelt fix. Launchpad is unchanged: the app stays 2.1.2 and
still accepts VPhone.bundle 2.1.0 or later. This release's Launchpad zip
is signed but not notarized; the notarized 2.1.2 download still works.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A guest restored by this project is hacktivated, so it never receives an
activation record and online-auth-agent can never obtain the device
identity an authorization request is signed with. libmis's
checkTrustAndAuthorization therefore returns 0xE8008026 and the profile
stays in "Profile Needs Network Validation" for good: an app signed with
a free personal-team Apple Development certificate installs, then
refuses to launch, and Settings' "Verify App" cannot clear it because
the network step it offers is the step that cannot complete. A paid
team's profile is not marked as needing online authorization, which is
why this was never seen before.
DyldSharedCacheMISTrustAuthPatcher short-circuits the function to return
success, writing mov x0, #0; retab after the prologue's pacibsp so the
PAC pair stays balanced. The function is static and carries no symbol,
so it is anchored on the log string that names it outright and then
required to seed 0xE8008026 in its prologue before anything is written:
two independent routes that must agree. Replacement bytes are the
existing keystone-checked ARM64.movX0_0 and ARM64.retab constants, and
the modified page is re-attested. A cache whose libmis lacks the string
reports absent and exits 0, an already-patched cache is a no-op, and a
cache with the string but no seeding prologue is an error rather than a
guess.
The declaration mis_trust_auth carries no applicability and is not boot
essential: the guest is hacktivated on every base, so the failure exists
on every base. cfw install applies it unconditionally.
Validated on a fresh iPhone17,3 26.6.2 (23G90) + cloudOS 26.4 guest: the
patcher reached the same site through the DSC chunk path that was derived
statically from the extracted library, the guest booted normally, and a
free-team app now installs, verifies, launches and accepts an Xcode
attach.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Lead with what vphone-cli does, collect requirements in one place, link
the latest Launchpad release, and fix the bootstrap menu path (Apps, not
Guest). The Chinese text is the source; English, Japanese and Korean
follow it section for section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bundle 2.1.5 (build 16). Launchpad is unchanged: it stays 2.1.2, still
accepts VPhone.bundle 2.1.0 or later, and the READMEs keep pointing at
the 2.1.2 notarized Launchpad.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The window used to wait for the guest, then jump to its new shape with
a black frame between. The container now drives one 0.35 s ease-in-out
turn from a display link, interpolating the panel's angle and the window
frame together. Mid-turn the panel keeps its aspect ratio and shrinks
to fit inside the window, so there are no bars or black frames; full
screen turns the panel alone.
Rotations started from the menu set the orientation before the guest is
asked, so the window turns alongside the guest instead of after it. A
refused orientation moves straight on to the next candidate, and only a
full refusal turns back. The poll pauses while a menu rotation is
pending, so a read from before the guest turned cannot undo it, and a
repeated ⌘← or ⌘→ turns on from the orientation already being turned
to.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bundle 2.1.4 (build 15). Launchpad is unchanged: it stays 2.1.2, still
accepts VPhone.bundle 2.1.0 or later, and the READMEs keep pointing at
the 2.1.2 notarized Launchpad.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vphoned gains display.orientation, which asks SpringBoard's
activeInterfaceOrientation through AXSpringBoardServer instead of
capturing the screen, so the host can poll it every second. Guests that
report display_orientation are polled; others stay portrait.
The window's content view now holds the VM view turned to that
orientation. The VM view keeps its portrait bounds, so touch mapping is
unchanged: AppKit's conversion undoes the rotation. A windowed VM swaps
its sides around its center and shrinks to fit the screen; full screen
letterboxes the turned panel. A frame saved while sideways is turned
back to portrait at launch.
The Device menu gains Rotate Left (⌘←), Rotate Right (⌘→) and an
Orientation submenu checked by the current orientation, enabled only
while the agent reports display, so the keys otherwise reach the guest.
An orientation the front app refuses is skipped by the rotate keys and
reported by the submenu. New strings are translated for ja, ko, vi and
zh-Hans.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Edit menu's Copy, Cut and Paste now reach the VM view, which carries
the clipboard through vphoned: paste sends a changed Mac clipboard to the
guest before the guest pastes, and copy and cut bring the guest clipboard
back once the guest has written it. Only small text and images are
synced (VPhoneClipboardTransfer); anything else stays on its own side.
Without a connected agent the items are disabled and the keys reach the
guest unchanged.
capturesSystemKeys makes VZVirtualMachineView swallow every key in a
local event monitor it re-adds on each focus change, so a monitor of ours
always ran after it. VPhoneApplication asks the main menu in
sendEvent(_:), which runs before any local monitor, and drops the key-up
of a key a menu item took.
A full-screen VM letterboxes the guest display, so touches measured
against the view mapped the bars onto the guest screen. They are now
measured against the drawn display (VPhoneDisplayGeometry): points on a
bar clamp to the nearest edge and count as near it for edge swipes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
swiftformat's pass over three Launchpad files: an explicit self, two
redundant type annotations, a try hoisted out of a call, an operand
order and blank lines. No behaviour change, so Launchpad stays 2.1.2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Device keeps the phone's buttons, input and Restart Guest; a new Features
menu holds the Location, Battery and Camera overrides. Guest becomes Data:
browsers, preferences and every clipboard action, including typing the Mac
clipboard. Bootstrap install and uninstall move to Apps, and Ping and the
agent hash go into a Guest Agent submenu under Diagnostics.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bundle 2.1.3 (build 14). Launchpad is unchanged: it stays 2.1.2, still
accepts VPhone.bundle 2.1.0 or later, and the READMEs keep pointing at
the 2.1.2 notarized Launchpad.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The window buttons, the title and the Home button now share one gap of
12 pt: from the window edge to the close button, between the three
buttons (AppKit uses 9 pt), from the zoom button to the title and from
the Home button to the edge. AppKit puts the buttons back at its own
inset whenever it lays out the title bar and may replace them when the
window is shown, so they are placed again after each frame change and
after resize, full screen, key, main and screen changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dropping files on the VM window used to take only the first .ipa or
.tipa. Every dropped file now goes through vphoned: a package is
installed as before, and any other file is uploaded and moved into the
Files app's On My iPhone › vphone-drop. Folders are not taken, and one
alert sums up the drop.
On My iPhone is `File Provider Storage` in the
group.com.apple.FileProvider.LocalStorage app group, whose container
UUID differs per device, so vphoned finds it by the container metadata
(files.save_to_files_app, capability files_app_drop). The Files app shows
an item placed there at once when it looks like one the app creates:
owned by mobile, folders 755, files 644, no extended attributes, which
is what a folder made in the Files app has. A name already taken is kept
and the new file is numbered, as in "notes 2.txt".
Tested on a RootHide guest (iOS 26.6.2): a screenshot dropped on the
window and two files saved under one name through the RPC appear in
Files as vphone-drop with three items.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The unified toolbar leaves a wider gap after the zoom button than the
close button's inset from the window edge. The window now hides its own
title and draws the name and subtitle in the titlebar, with the gap after
the zoom button held equal to that inset by two layout guides. The title
truncates before the Home button. window.title and window.subtitle are
still set for the Window menu and accessibility.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once vphoned connects, the VM window's subtitle reads
`iOS <version> - <address>`, and it is empty while vphoned is away.
vphoned's health report took the first en* address, which sorts to en0's
fe80:: link-local address. It now picks a 192.x IPv4 address first, then
any other IPv4 that is not loopback or 169.254, then a routable IPv6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
icli before 0.7.4 leaked every launchd reply, and a load reply carries the
job's listening sockets. vphoned loads the bootstrap's daemons at boot and
lives until shutdown, so sshd's port stayed bound after an openssh
upgrade booted it out: the reload failed to bind port 22 (launchd logged
`assertion failed ... 0x30`) and the guest had no SSH until a reboot.
Killing vphoned freed the port, which is how the holder was found.
Tested on a RootHide guest with vphoned built against 0.7.4: two rounds
of dpkg -i, dpkg -r and dpkg -i of openssh-server and two rounds of
services.unload/services.load of sshd, with sshd answering after each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The launchd hook used to add the bootstrap's Library/LaunchDaemons to
launchd's cache under /System/Library/LaunchDaemons/vphone.<name> keys. A
job imported that way does not match the plist path a package script boots
out, so upgrades could not unload their own daemons. RootHide's own hook
loads only basebin/LaunchDaemons and leaves the rest to `jbctl startup`.
The hook no longer interposes xpc_dictionary_get_value. After boot vphoned
loads each plist in <root>/Library/LaunchDaemons under its real path. For
RootHide it first rewrites the plist on disk the way RootHide's launchctl
does (plistpatch.m, ported to Swift): Program, ProgramArguments[0], the
working and root directories, standard streams, watch and queue paths,
HOME/TMPDIR, KeepAlive.PathState, socket paths and fsevents paths get the
root prepended, and __Patched marks the plist. services.load patches a
plist under the root the same way, so no launchctl binary is needed.
vphoned installs RootHide under one fixed name,
.jbroot-000114514191980C, and the hook looks only there and in /var/jb.
A RootHide root under another name must be uninstalled first.
Tested on a RootHide guest (iOS 26.6.2): every daemon plist carries
__Patched and is registered under its own path, ssh and sudo work, and
launchctl bootout/bootstrap of ighostvtd and a dpkg install, remove and
reinstall of openssh-server all load the job again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The accessory container had no frame width, so the button collapsed to
zero and showed an empty glass shape. Size the container explicitly and
keep the button square. On macOS 26 it is a circular glass button.
The disconnected icon is now circle.circle with a slash drawn across it,
since SF Symbols has no circle.circle.slash, instead of circle.slash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The window title is the VM's folder name and the subtitle is gone; the
connection state no longer appears in the title. The Home button moves
from the toolbar to a trailing titlebar accessory so a narrow window
truncates the title instead of hiding the button in overflow. While
vphoned is not connected the button shows circle.slash and is disabled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bundle 2.1.2 (build 13) and Launchpad 2.1.2. No new CLI surface or
helper change since 2.1.0, so the minimum bundle version stays 2.1.0 and
the helper build stays 8. The READMEs point at the 2.1.2 notarized
Launchpad.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#519: vphoned wrote jbroot/dev as the link text /rootfs/dev. The kernel
resolves link text, not vroot paths, so it dangled: every shell failed on
/dev/null and sshd never started. It is now /dev, an existing
/rootfs/dev link is replaced, and rootfs -> / is created.
#520: Irisin unpacks packages without RootHide dpkg's hook, so nothing
linked .jbroot in deeper package directories, and sudo could not load
libsudo_util from usr/libexec/sudo.
- vphoned walks the bootstrap for directories holding Mach-O files and
links each one, at install, at startup, and one second after the root's
Library/dpkg changes. That pass also runs the base steps that waited for
pwd_mkdb or ssh-keygen, so sshd has host keys once openssh is installed.
- The spawn hooks follow the executable's LC_RPATH and LC_LOAD_DYLIB
entries inside the root and link each dependency's directory, within a
fixed bound. SystemHook does the same for TweakLoader before its dlopen.
- launchd starts xpcproxy and bootstrap daemons through posix_spawnp, which
the launchd hook now interposes. SystemHook is chain-loaded into every
child whatever its environment; DISABLE_TWEAKS and safe mode only keep
ElleKit out.
The installer moves to Daemon/Bootstrap, with RootHide and rootless code
in their own folders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VPhoneEscalator.c keeps the header comment and main(). Escalator.h holds
the shared paths, keys, globals and declarations; AMFIDTask.c attaches to
amfid and reads and writes its memory; Requirements.c builds the cdhash
requirement; Preferences.c owns the coderequirements preference; and
Commands.c holds the status, allow and off verbs. No behavior change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bundle 2.1.1 (build 12) and Launchpad 2.1.1. No new CLI surface or
helper change since 2.1.0, so the minimum bundle version stays 2.1.0 and
the helper build stays 8. The READMEs point at the 2.1.1 notarized
Launchpad.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Machine table: a search field beside New Machine filters by name,
iOS version or build, UDID and volume; headers sort by name, location,
iOS, CPU, memory and disk.
- Toolbar: Start/Stop and the actions menu sit with the inspector at the
trailing edge and hide with it; New Machine and search keep their own
glass capsules.
- Patch Settings is gone from existing machines: a machine's patches are
fixed once it is installed, so the sheet is New Machine only.
- Patch sheet: aligned header, native search field, no per-row boot
icon (only a warning when a required patch is off), shorter status.
- Recent Commands: fixed icon and time columns, and a Copy button.
- Translations for every new string and for the patch sheet's strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Iterating bytes(for:) one byte at a time made downloads CPU-bound. A
URLSessionDataDelegate now writes each chunk straight into the .partial
file on the cache's own volume, so there is no temporary copy on another
volume either.
Fixes#524
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--disk-size and Launchpad's stepper say GB, but Disk.img was truncated
to GiB, so a 128 GB machine showed 137 GB inside iOS. Disks are now
created at N × 10^9 bytes, and vm list and Launchpad divide by 10^9,
matching iOS. Existing machines keep their size and now show it in the
same unit iOS does (a former "64 GB" disk reads 68 GB).
Fixes#523
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rename mapped and searched every 64 MiB window up to the image's
logical size. A 128 or 256 GB disk has under 10 GB written, so most of
the time went to faulting in zero pages from holes. Each window now
asks SEEK_DATA first and jumps to the window holding the next data;
windows stay aligned, so scan() sees the same blocks. A volume that
cannot report holes falls back to scanning every window.
Fixes#522
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>