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>
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>
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>
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>
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>
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>
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>
`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>
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>
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>
Two ways to drive the guest from the host, both landing on the same touch injection.
Trackpad: a two-finger scroll becomes a synthetic finger that presses under the pointer and follows the physical direction, with edge re-anchoring so a long swipe is not bounded by where the pointer sits, and a Home-strip snap so an unlock gesture starts inside the indicator strip. A pinch becomes two fingers spreading or closing around the pointer. The two never interleave.
Esc: iOS has no back key, and a forwarded Escape only reads as cancel (or stop-loading in Safari), so Esc and a new Device -> Back item replay the system back gesture instead. It is taken in VPhoneApplication.sendEvent, before the menu lookup, because AppKit does not reliably match a modifier-less Esc key equivalent.
Both land on the view existing touch path, so the guest-side half is shared: VPhoneGuestControl gains supportsMultiTouch and sendTouch2 for the new input.touch2 request, and VPhoneDaemon builds the two-finger hand event itself because icli carries one digitizer point per event and cannot express a pinch. A guest that predates touch2 degrades to a one-finger move.
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>
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>
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>
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>
#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>
Issue #438: guests built after the former EXP patches joined the JB flow
lost location. standard is now the JB baseline plus the virtual camera.
- watchdogd.hv_vmm_cache joins the hv_vmm_present concealment group and
loses bootEssential: watchdogd only panics once the OID is renamed.
- The eight DeviceTree identity rewrites and the Preboot DeviceTree
rewrite, newly declared as preboot_devicetree.identity, are blocked in
standard. cfw install now asks the plan before the Preboot rewrite.
- Camera DT nodes, cam offsets and camera_dsc stay on.
- libvlocation.dylib and its SystemHook load are removed. location.set,
clear and current call IcliKit directly again, which confirms a request
by reading back a fresh, software-simulated fix at that coordinate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The proxy's exponential backoff kept the guest API down until about
24 s after launchd started it: early-boot workers exited and the proxy
waited 1, 2, 4 and 8 s between them. Retry after a fixed second, for as
long as the proxy runs, and log each exit's status or signal.
The plist sends stdout and stderr to /var/log/vphoned.log, where those
lines were previously discarded, sets ThrottleInterval to 1 so launchd
restarts the proxy after a second, and marks the job Interactive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`fw patch --preset standard` spent 28.8 s of 49 s inside one patch.
Sampling a Release run (8623 samples) put 93% in the vm_map_protect
Shape C scan and 72% of that inside `cs_disasm` — not decoding, but in
Capstone's printer: printInst / printAliasInstr / matchAliasPatterns,
vsnprintf / SStream_concat, and map_set_alias_id -> name2id's strcmp
chain. The real decode, AArch64_LLVM_getInstruction, was 6%.
The scan is deliberately unscoped, so it walks all 8.4 MB of kernel text
and was decoding five instructions at each of ~2.1M offsets only to
reject nearly all of them on the first one.
Gate it on the raw instruction word first, the way buildADRPIndex and
the vm_map_delete scan already do. The gate only ever rejects: a word
that survives takes exactly the decode and the checks it always did, and
every positive determination stays Capstone's.
Both encodings of `mov wD, #6` are accepted even though only MOVZ can
reach the existing `mnemonic == "mov"` check — the MOV-bitmask alias
applies only when the immediate is not MOVZ-encodable, so
`orr w9, wzr, #6` (0x321F07E9) prints as `orr`. Letting it through
anyway keeps the gate independent of that aliasing rule, which is the
one way a cheap prefilter could silently narrow the match. ARM64InstTests
pins both words and the printer's answer for each.
patchThreadSetStateEntitlementFlag has the same shape (extended only) and
gets the same treatment via isBorBL.
Release, 17,3_26.4 + cloudOS 26.4: the patch step 28.81 s -> 1.28 s,
whole run 48.98 s -> 21.68 s standard, ~61 s -> ~28 s extended,
instructions retired 1.006e12 -> 5.38e11.
Verified byte-identical: four bases (26.1 / 26.4 / 26.6.2 / 27.0) x both
presets, HEAD vs HEAD+prefilter, every file in each restore tree hashed —
12/12 identical, same 172 standard / 183 extended counts, same 0x1DC6EA0
rewrite, no undeclared-patch warnings. FirmwarePatcherTests: 410 tests,
132 issues, unchanged from HEAD and all missing local reference samples.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It bricks a freshly restored 26.4 guest. bluetoothd caches its
sysctlbyname("kern.hv_vmm_present") answer in a dispatch_once (23E246:
the call at 0x1004ac7b4, dispatch_once at 0x1004ac798, flag at
0x100b51bc0); with the OID renamed it gets ENOENT and caches 0, so its
chip-selection singleton (0x10042fa40) picks a transport from the
MGIsDeviceOneOfType table instead. Nothing there matches a virtual
iPhone, the transport singleton at 0x100b50bd0 stays NULL, and it faults
on `ldr w23, [x0, #0x31c]` at +0x402864 and crash-loops until launchd
throttles com.apple.bluetoothd. locationd's CLSeparationAlertsServiceSilo
then blocks on a synchronous call to that throttled service, the
com.apple.locationd.migrator plugin hangs for over an hour, and
SpringBoard waits on migration: black screen, no panic, nothing in the
log naming the cause.
The two halves — kernelcache_exp.hv_vmm (kernel OID rename plus the
kernel-internal cstring mangle) and hv_vmm_dsc (the shared-cache mangle
the system installer runs) — are one behaviour in everything but name,
and neither is useful alone: the rename without the mangle breaks the
graphics and ML paths, the mangle without the rename does nothing. They
are now hypervisorConcealmentPatches, blocked by standard and present in
extended, and a catalogue test refuses any shipped preset that enables
one without the other. Both lose bootEssential, which they never were:
a shipped preset that drops a boot-essential patch warns on every run.
buildComponentList no longer builds KernelExperimentalPatcher when the
patch is off, rather than building it and letting the gate refuse the
write — a patcher constructed to write nothing still logs as if it might.
The DSC half needed no code change; the installer already runs
patch-hv-vmm-dsc only when the plan enables it. A VM with no recorded
plan still gets both, deliberately: that is what its guest was restored
with.
Everything else the former EXP integration brought over stays on:
DeviceTree identity and camera, camera_dsc, the watchdogd cache patch,
and the post-restore Preboot DeviceTree rewrite.
The user-mode xref inventory is corrected too. It listed bluetoothd as
carrying the string with no reference to it, which was measured on 26.1
(23B85) and is not true of 23E246 — so the line is marked per-build
rather than deleted, the 26.4 call sites are written down, and the
"standalone binaries fall into unpatched → ENOENT → cache 0
automatically" step in the shipping design is flagged as the bug it is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Patches were scattered across the patchers that applied them: nothing listed
what a run would do, and nothing could turn one off. This adds the manifest
layer that names them and the preset layer that chooses.
VPhonePatchKit is a distribution framework (library evolution on) holding the
model: VPhoneVersion, VPhoneVersionRequirement, VPhonePatchDeclaration,
VPhonePatchSetManifest, VPhonePatchPreset, VPhonePatchPlan and the gate a
patcher consults before each write. Capstone and the ARM64 encoder moved in
behind an internal import, so nothing downstream sees the package.
Ten bundled sets in FirmwarePatcher/PatchSets declare every existing patch,
checked against the patchers by FirmwarePatchSetCatalogTests. Two presets ship
in VPhone.bundle: standard, and extended for the experimental sets. A
version-pinned patch is present in the manifest but off unless a preset or a
per-VM checkmark asks for it, and neither can widen its version gate.
Patch sets also load from outside the tool. A .vphonepatchset is a macOS
loadable bundle whose Contents/Resources/Manifest.plist is read before any of
its code is mapped, and whose executable exports one symbol,
vphone_patch_set_principal, returning a VPhonePatchSetPrincipal that hands the
pipeline one BufferedPatcher per component the plan enabled.
Not NSPrincipalClass, which is how a loadable bundle normally names its entry
point: library evolution makes VPhonePatchSetPrincipal a resilient superclass,
so a subclass of it needs runtime metadata initialization and is not registered
with the ObjC runtime when the image is mapped. NSClassFromString cannot find
it, and Bundle.principalClass then silently returns whichever class was
registered — the example set's patcher rather than its principal. A @_cdecl
symbol found with dlsym has none of that.
External sets are boot-chain only, because root cfw install loads no external
set; a preset that names one lives in ~/.vphone/patches_presets and cannot
shadow a shipped identifier. `vphone-cli patchset import` copies a set into
~/.vphone/patchsets and ad hoc signs it if it arrived unsigned, so a bundle
straight out of Xcode loads: the linker signs its Mach-O but seals no
resources, which codesign rejects until one pass over the bundle fixes it. The
signature is re-checked from disk at every load, and PatchSetLoaderTests proves
that over the example set — inspect, validate, tamper, load, patch, gate off.
BufferedPatcher replaces the nine concrete downcasts the pipeline used to read
patched bytes back with, which is what lets an out-of-tree patcher return any.
Launchpad gains a patch table per machine, `vphone-cli fw set-patches` writes
the selection, and Skills/authoring-patch-sets documents the whole flow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Post Darwin notifications from the Controls panel
vphoned gains notify.post {name, state?} and notify.state {name}, thin
calls into IcliKit 0.7.0's postDarwinNotification and
darwinNotificationState. The state is a full UInt64, so it is accepted
as a decimal string as well as a number.
The Controls panel gets a Darwin Notification section: a name field with
presets, an optional state, and Post and Read State buttons. This
replaces #248, which targeted the removed ObjC daemon and sources/ tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refuse a JSON boolean as a notification state
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
sshd's "PAM: initialisation failed" came from the modules in usr/lib/pam
having no .jbroot link, which #515 now seeds. Keep the fresh-restore check
with the package's sshd_config as the remaining validation step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Irisin's RootHide bootstrap unpacks packages, but nothing created what a
jailbreak's bootstrap installer normally does. iGhostVT hung on a missing
/tmp, /dev/null was unreachable under vroot, every getpw* lookup failed
with EINVAL, and sshd reset connections for lack of host keys.
ensureRootHideBase runs beside ensureRootHideLinks at install and on
startup. It creates tmp, var/tmp, dev, var/root and the account files,
builds pwd.db and spwd.db with the bootstrap's pwd_mkdb, and runs
ssh-keygen -A once openssh is installed. Existing items are left alone,
and the databases are rebuilt only when older than master.passwd, so a
changed password survives. Steps whose tool is not unpacked yet are
reported as deferred and run on the next start.
Co-Authored-By: McNight <mcnight@mcnight.fr>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Publish validated location state to authorized CoreLocation clients, keep guest hooks in sync, and add a Release artifact workflow for unsigned CI packages.
A repo-wide security review found issues in the root CFW install, the
Launchpad helper, VM bundle handling and the guest boundary. This fixes
them and applies swift format.
- cfw install mounts guest volumes nosuid,nodev,nobrowse in a root-only
temp folder and does every guest read and write through an open folder
handle, never following links. BuildManifest cryptex paths must stay in
the restore folder, and only a private copy is attached.
- Root no longer chowns or chmods the whole VM folder after an install.
The shared walk skips hard links, symlinks, special files and other
volumes.
- Every Launchpad helper action needs administrator authorization. Only
the user who started a CFW install can cancel it. The helper refuses
setuid, hard-linked, special or escaping-symlink entries in a bundle.
- Manifest file names must be single names in the bundle and point to
regular files. vm import refuses links that leave the bundle.
- Guest file names from the file browser, QuickLook, drag-out and crash
logs are validated and written exclusively, without overwriting, and
are quarantined.
- The guest HTTP client no longer traps on a bare Content-Length, caps
bodies and enforces a per-request deadline.
- The --api-listen proxy needs a per-launch token. vphoned refuses
browser-origin and non-local Host requests.
- vphoned stops following links when it sets up Irisin and the camera
files.
Thanks to fresh-fx59 for reporting the guest file name, HTTP parsing,
cfw install and manifest path issues in #469.
Co-authored-by: Aleksey Aksenov <5788874+fresh-fx59@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The camera hooks were built and shipped but never reached the guest, so
Camera.app could not show a streamed frame. cfw install now places
libvcamcaptured.dylib and libcamfix.dylib in /usr/lib beside the launchd
hook and SystemHook. SystemHook treats /usr/libexec/cameracaptured as an
injection target and loads the daemon hook there, and loads libcamfix into
app processes that have AVFoundation loaded. Both hooks install their own
Objective-C method replacements, so neither needs ElleKit or a bootstrap.
A running guest gets changed copies of those four libraries through the
new vphoned environment update. environment.status reports the SHA-256 of
each library in /usr/lib; environment.install checks the uploaded copies,
remounts / read-write when needed, renames each library into place and
remounts / read-only again, because jailbreak detection reads a writable
root as rootful. It stops cameracaptured when a camera hook or SystemHook
changed and reports when the launchd hook needs a guest restart. The VM
process runs the update once per connection, after the vphoned
self-update, uploading only libraries whose hashes differ.
Not yet verified in a guest; the validation steps are in
Research/0_binary_patch_comparison.md and Research/vphoned_http_api.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run a small launchd-owned proxy that posix_spawns the same signed binary in --io mode, reaps/restarts it, and exits the worker when the proxy dies. Preserve cached guest updates with a bounded streaming hash.