The two gates between Xcode and a vphone guest
HomePod 27 port (2026-10-23)
TempPod 27.2/24K120 (AudioAccessory5,1) reached three independent installation gates through Xcode 27.2 Run, after the DDI and UserManager lookup repairs:
- The public tvOS SDK drops device family 5 while generating
UIDeviceFamily, even withTARGETED_DEVICE_FAMILY = 3,5. ItsSupportedTargets.appletvosDeviceFamiliesarray defines only Apple TV (3). - With the family repaired, a developer-signed app reaches an absent
com.apple.misagentservice and fails installing its embedded profile. HomePod ships neither the daemon nor its launch plist. The app instead uses ad-hoc signing through the user'sresearchsdkmechanism. - MIS accepts the local signature with the existing signature options, but
MobileInstallation then fails to extract a certificate signer identity
(
MIInstallerErrorDomain13,LibMISErrorNumber-402620415, verifier line 422). Its existingallowAdhocSigningproperty is needed too.
system-installd-exp-homepod_adhoc ports these two signature adapters to a
dedicated TVOS arm64e library loaded only into HomePod's installd. The
VPHONE_MIS_SIGNATURE_ONLY build excludes the embedded-profile bypass; no
identity, profile scope or general environment hooks are linked. Both native
validation stages still produce their real signature metadata. The patch is
27-only, enabled by homepod-mini, and blocked by the iPhone and TV presets.
Opt-out restores installd from .bak before removing /hm and the library.
The researchsdk generator at /Users/insidegui/Developer/_projects/researchsdk
now adds HomePod's numeric family to both SDKSettings formats in the cloned
tvOS SDK. It also selects Apple TV resource compilation because Interface
Builder rejects a homepod storyboard target. HomePodApp uses the full research
SDK path, TARGETED_DEVICE_FAMILY = 3,5, manual local signing and normal Xcode
CodeSign. Its temporary metadata/signing build phase was removed. Xcode
generates [3,5] itself, and codesign --verify --strict passes with no embedded
profile. The app also needs the standard application-identifier entitlement
($(PRODUCT_BUNDLE_IDENTIFIER) for this local research signature), in addition
to get-task-allow for LLDB. Without it, installd correctly returns error 63.
On TempPod with bundle 2.12.0-local.92401773, Xcode Run installed the app and
launched PID 230 under LLDB. process status and bt 5 returned a stopped
process and symbolicated system stack after the UIKit template asserted on its
missing HomePod display configuration. The installd capture
/tmp/temppod-homepod-signature-installer.json shows MIS result 0, native
CdHash, SigningID, SignerType=1, ValidatedByProfile=false,
IsNativeForPlatform=true, and “Install successful” in 0.05 seconds. A plain
UIScene cannot substitute for UIWindowScene under the application's system
role; UIKit reports that minimum-class constraint separately. These errors
demonstrate working launch/debugger communication, not support for HomePod UI.
The full VPhone bundle passes admission and 77 selected catalogue, selection, version, declaration, plan, gate and guest-resolution tests pass. The persona native ABI/policy harness also passes. SDK-family insertion was applied twice to scratch copies of Apple's settings: both JSON/plist formats retain exactly two dictionary entries (3 and 5) with no duplicate; shell syntax and plist validation pass.
Live opt-out restored installd byte-identically to its original backup
(SHA-256 6dd955e206ca3227ad28bda202718b7357c019e107ed79132f61a6a36d9b21cd),
and removed /hm and the HomePod signature library. The persona patch was
disabled in the same run and launchd also matched its original backup. TempPod
booted successfully with both adapters disabled. The normal preset was then
restored for the final installation/debugger check.
After restoring the preset (no overrides), Xcode installed and launched the
headless Foundation version as PID 210. It uses a run-loop timer rather than
UIApplicationMain, preserving the template UI source files for future work.
LLDB stopped at researchHeartbeat, main.m:8; p heartbeatCount returned 2,
thread step-over advanced it to 3, and
po [NSProcessInfo processInfo].processName returned HomePodApp. The breakpoint
was deleted and execution resumed. CoreDevice's process enumeration independently
listed PID 210, and ddiServices reported the 27B48 cryptex compatible and usable
after the reboot. Xcode's device picker identifies TempPod (Internal) with
the tvOS 27.2 Research SDK. Template window scenes remain unsupported because
HomePod has no display; the headless process is the validated research target.
The process continued logging five-second heartbeats after resuming, without
the earlier UIKit assertion. It is left running under Xcode's debugger, and
the temporary MIS LogQueries configuration has been removed. Olive and the
default bundle were not changed.
Original iPhone investigation
Measured 2026-09-30 on 05-mis-test (iPhone base 26.6.2 / 23G90, cloudOS 26.4
/ 23E5207q, jb variant), booted, with mis_trust_auth applied.
xcrun devicectl device install app refuses two different things, in two
different processes, and they need separate answers:
Everything behind both refusals already accepts unsigned code, so neither is a deep problem — each has one cause and one place to answer it.
Part one is the signature. Part two, further down, is the device identity.
What was measured
A throwaway app (wiki.qaq.vphone.signtest, one UIKit view controller) built
with no team, no provisioning profile and no Apple certificate, offered to the
guest three ways:
All three fail in the same frame:
which calls MISValidateSignatureAndCopyInfo in /usr/lib/libmis.dylib.
Everything past that gate already passes
The same app, pushed in through vphoned's apps.install — which re-signs in
the container with vp_sign_app_for_install and registers the bundle itself,
never touching installd or misagent — installs and runs:
So the kernel side (amfi_trustcache, jb.post_validation,
jb.amfi_execve, jb.cred_label_update_execve), lsd registration and
SpringBoard's launch path all accept a bundle with no certificate and no
profile. Only installd's install-time check does not.
That also means SpringBoard does not reject an ad-hoc signature at launch.
The 0xE8008026 SpringBoard logs in the free-certificate case is a different
gate — online authorization — and is what mis_trust_auth covers.
libmis takes an option for exactly this
MISValidateSignatureAndCopyInfo's options dictionary is read by string key.
The keys are in libmis's cstring section and are, in cache order:
and the keys it returns in info:
There is no SDK header for these; the symbols kMISValidationOption* are not
in libmis's export trie, so searching for the symbol name finds nothing and
searching for the string value finds everything.
A probe app — unsandboxed (com.apple.private.security.no-container,
no-sandbox), so it could read the subject out of /private/var/tmp — called
the function directly against the codesign --sign - bundle:
Two things follow. The option alone is sufficient — installd simply never
passes it. And MIS fills the whole info dictionary itself on success, so
nothing has to be synthesised; a hook that faked a return code without
CdHash and Entitlements would break installd further along.
Against the ldid-shaped signature the project's own signer writes, the same
call returns 0xE800801C with or without the option. "No signature at all" is
therefore out of reach and is not worth pursuing: everything downstream needs
the cdhash only a signature carries, and Xcode's "Sign to Run Locally"
(CODE_SIGN_IDENTITY = "-") supplies one for free.
Two details that cost time
The first argument is a path string, not a URL. Handing it an NSURL
aborts the calling process inside libmis with
-[NSURL length]: unrecognized selector sent to instance.
On arm64e the __DATA,__interpose section the compiler is asked for lands in
__AUTH_CONST instead, because its pointers are signed. dyld honours it
anyway — verified by loading the real libmisfix.dylib from a probe app's own
bundle through a load command and calling the linked symbol:
What was built
system-installd-cfw-adhoc_signature — VPhoneGuestComponents/MISFix/MISFix-vphone.c,
built as /usr/lib/libmisfix.dylib, attached to /usr/libexec/installd by a
LC_LOAD_WEAK_DYLIB that cfw install inserts. (Superseded 2026-09-30: the
declaration is gone, and the spawn hooks insert libmisfix into installd,
misagent and SpringBoard — see "One route for libmisfix" in
Research/0_binary_patch_comparison.md.) It interposes
MISValidateSignatureAndCopyInfo and
MISValidateSignatureAndCopyInfoWithProgress, adds AllowAdHocSigning to the
caller's options, and calls through. Nothing in the dyld shared cache is
touched — deliberately, since issue #532 is mis_trust_auth writing a cache
code page and leaving a 27.0 guest unable to boot.
Still open
MICodeSigningVerifier lives in MobileInstallation, a shared-cache dylib, so
its call into libmis is cache-internal. dyld's interposition is documented to
patch the cache's own uses of an interposed symbol, and the probe above
confirms the section shape is honoured on arm64e, but the cache-internal case
itself has not been observed yet — that needs the hook running inside installd
on a guest, which is the 27.0 integration test.
Only installd carries the hook. SpringBoard was left alone on purpose: the evidence above says it does not reject an ad-hoc signature, and a mistake there is a black screen rather than a failed install.
Part two: the device the profile names
A real, paid-team app fails a step later. AirBuild-Debug-main-da1b84d8.ipa,
signed Apple Development: Lakr Aream (ZL3L65D2SL), team QDJ93ZUQ9B, whose
embedded profile provisions eight real devices and no VM:
This one is correct behaviour and always has been: the VM's UDID is in nobody's
ProvisionedDevices. Xcode papers over it for a free personal team by
registering whatever device is plugged in — which is why
iOS Team Provisioning Profile: wiki.qaq.vphone.test (team M4Z5DVY94F) lists
both 0000FE01-04A9F39591FF643D and 0000FE01-5F651C7ECC040381. For a paid
team there is no auto-registration, and a fresh vm create means doing it
again by hand.
Which process decides, and what it asks
installd reports the error but does not make the decision; misagent does. Its imports and strings give the whole algorithm away:
No MISProvisioningProfileIncludesDevice — misagent reads ProvisionedDevices
itself and does its own comparison. Its strings, in the order the code uses
them:
So it first looks for an emulated UDID in the kernel's codesigning configuration, then falls back. Measured in the guest:
security.codesigning.config is a four-byte flags word, not a dictionary — it
cannot carry a UDID, and amfi_emulate_device_udid appears in neither the
vphone600 kernelcache nor TXM. That path is dead on this board, so the
MobileGestalt fallback is what always runs.
MGCopyAnswer is exported from libMobileGestalt, and misagent imports it
across an image boundary, so it is interposable.
Where the guest's own UDID comes from
Worth writing down, because it rules out the alternatives. TXM builds it, before the kernel runs, out of the device tree:
TXM's strings: successfully queried the UniqueDeviceIDv2 from the device tree, unsupported UniqueDeviceID version: %u, unable to find udid-version property in /product.
The host predicts the same string from the same inputs —
VPhoneVirtualMachine.resolveDeviceIdentity reads _ECID off the
VZMacMachineIdentifier and formats "%08X-%016llX" against
VPhoneHardware.udidChipID = 0xFE01, then writes udid-prediction.txt. The
prediction is reliable because both sides compute it the same way.
Two consequences. data_ark.plist does not hold it — a live guest's copy
has eleven keys and UniqueDeviceID is not among them — so no daemon can
change it by writing a file. And reproducing a real iPhone's UDID is
impossible even in principle: chip-id is fixed at 0x0000FE01, while a real
device's is something like 0x00008150, and unique-chip-id is the ECID the
guest's SHSH blob is issued against, so changing it means restoring the VM
again.
What was built
system-misagent-cfw-device_identity — the same libmisfix.dylib, attached to
/usr/libexec/misagent (now inserted at spawn, like installd's; the
declaration is gone), interposing MGCopyAnswer and MGCopyAnswerWithError
and answering UniqueDeviceID with the value in libmisfix.plist. Set it to a
device the team has already registered and that team's profiles install here,
with no portal round trip and nothing to redo after a rebuild. Absent or empty,
the hook is inert.
The settings file is read from /var/db/vphone/misfix.plist first and
/usr/lib/libmisfix.plist second, cached against mtime and size so an edit
takes effect on the next query with nothing restarted. The data-volume path is
the one a running guest can be reconfigured through; the /usr/lib copy is
what cfw install ships, and it is only written when the guest has none, so
re-running the installer never puts an empty file over a UDID someone set.
The VM window sets it from Device › Set UDID… and Reset UDID, through
vphoned's udid.set and udid.clear (Research/vphoned_http_api.md). vphoned
writes the data-volume file, reads it back and restarts the hooked daemons.
The host sees it too (2026-09-30)
This section used to say the host keeps seeing the guest's own UDID, and that making the two agree needed a re-restore. It needs neither. The host learns the UDID in three places, all of them in userspace or reachable from it:
The kernel builds the USB serial itself from /chosen chip-id and
unique-chip-id (%08X%016llX); IOPlatformSerialNumber is vphone-1337
and plays no part. vphoned starts before the USB device exists, when the
controller has no description to change, so it retries in the background
until it can.
Measured on test-27.0 with 00008150-00112233445566AA: idevice_id,
ideviceinfo -k UniqueDeviceID and remotectl show (both the
virtualmachine and the ncm transports) all report the override, after
udid.set and after a reboot; udid.clear puts all three back.
ideviceinstaller list works under the new identity.
Two things turned up on the way. MGCopyAnswerWithError takes three
arguments (question, options, error), and the hook had declared two; misagent
never calls that spelling, remoted does, and it crashed at 0xe until the
prototype was fixed. And remoted runs with EnableTransactions, so SIGTERM
does not stop it, and it is launched by the NCM link coming up, not on
demand; vphoned sends SIGKILL and always re-enumerates.
What stays the guest's own is TXM's and the kernel's view, and CoreDevice's
record of a pairing made under the old UDID. A new UDID is a new device to
the host, so the guest asks to trust the computer again; it keeps one
lockdown pair record per host, so switching back asks again too. Pairing
with devicectl itself is not covered here: it times out against vphone
guests for reasons unrelated to the UDID.