Hiding Apple-ID sign-in surfaces and VM-unavailable features
Feasibility study (2026-10-07) on an iOS 27.0 guest (iPhone17,3, 24A435), never signed in, no Apple ID. Two questions:
- Can every Apple-ID / iCloud sign-in surface across the system apps be hidden?
- For features the VM does not provide, can the matching Settings entry be hidden?
Static analysis of the 27.0 dyld shared cache
(/private/tmp/va-analysis/dsc27/24A435__iPhone17,3/dyld_shared_cache_arm64e),
app binaries extracted from the IPSW, Preferences.bin, and the guest device
tree. Companion to settings_app_row_hiding.md (the SwiftUI Settings-row binary
patch, a separate work-stream) and post_setup_signin_and_appstore.md.
The one finding that shapes everything: ENTRY ≠ SHEET
The sign-in sheet (the credential UI) is drawn out of process by remote
view-service apps (AuthKitUIService.app, AppleIDSetupUIService.app,
AAUIViewService.app, AMSUIAuthenticationViewService, StoreKitUIService.app,
GameCenterUIService.app). An app only hosts it. So:
- Suppressing the shared sign-in machinery kills the sheet — tapping any "Sign in" does nothing — but does not remove the entry banner/cell the app itself draws. The banner stays; it just becomes inert.
- Removing the entry (the banner/row the user sees) is per-app work, except where the entry is an embedded shared controller (Settings' Apple-Account card; the App Store account tab) — those vanish with the sheet hook.
And there are two independent shared sheet paths, not one:
A hook that only covers AuthKit/AppleAccountUI misses the "Sign in to iTunes Store / Media & Purchases" sheet — the store path needs its own hook.
Sign-in SHEET suppression — BUILT and verified (libsigninfix.dylib)
Shipped as libsigninfix.dylib (VPhoneGuestComponents/SignInFix/libsigninfix.m),
loaded by SystemHook into app processes only (vpIsAppPath, never daemons, so
background AuthKit auth is untouched); wired into VPhoneGuestEnvironment.libraries,
GuestAPI+Environment.environmentLibraries, StageBundle.sh and ValidateBundle.sh
like the other hook dylibs. Installed per the selectable patch
system-apps-cfw-signin_sheets (on in standard). Resolves every class+selector by name at load (the
libhapticsfix pattern); re-runs on _dyld_register_func_for_add_image so the
lazily-loaded frameworks (AuthKitUI, AppleIDSetupUI, GameCenterUI, AMSUI) get
hooked when they map. Verified on signintest-iphone27 (iOS 27.0, iPhone17,3).
The iOS 27 reality that the earlier plan got wrong. The Settings "Apple
Account" card does not present a plain ObjC AAUISignInController, and
prepareInViewController:completion: is not the seam. Tapping the card
presents a SwiftUI .sheet: a _TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_
is presented through the normal UIKit present funnels, and the real controller —
AppleIDSetupUI.SignInOptionsViewController (the "Choose the method to sign
in… / Use Another Apple Device / Sign in Manually" chooser), wrapped by
AppleIDSetupUI.NavigationControllerReader.Reader<…SignInOptionsViewWrapper…> —
is a SwiftUI-hosted child that is not attached to the host when present: is
called. So a present-time block sees only the opaque AnyView host and cannot
decide. (Confirmed by logging all present funnels + loadViewIfNeeded: the host
always logged "pass".)
iOS 27's UIKit present funnels, for reference (from a runtime
class_copyMethodList dump): the public presentViewController:animated:completion:
→ _presentViewController:animated:completion: →
_presentViewController:withAnimationController:completion: → the lowest funnel
_presentViewController:modalSourceViewController:presentationController:animationController:interactionController:handoffData:completion:.
(The older guessed _presentViewController:withAnimationController:interactionController:completion:
does not exist on 27.)
What the dylib actually does (two seams):
Recognition is name-based (kSignInVCClasses + strong substrings SignIn,
AppleIDSetup, TapToSignIn, DeviceToDevice, …), so new sign-in classes are
caught without new addresses. The own-method swizzle uses class_addMethod-first
so it overrides only SignInOptionsViewController, never a shared superclass.
Known limitation (characterised, not a bug). For the SwiftUI Settings sheet the block is a dismiss-as-it-appears, not a never-present: the sheet is torn down before it finishes animating in, so a blank panel flashes (the content view is hidden first, so the sign-in chooser itself is never shown). The present-seam block for plain-UIKit sheets is a clean never-present.
A truly zero-flicker kill of that one card means hiding the row, not blocking
the sheet — and the lever is not an MDM restriction. Live RE (see
settings_app_row_hiding.md:30-40) proved the primaryAppleAccount card
consults no ManagedConfiguration feature: on root render Settings queries
only allowBluetoothModification and allowWritingTools, never
account-modification. allowAccountModification / MCFeatureAccountModificationAllowed
gates the Setup Assistant Apple-ID pane (setup_assistant_pane_gating.md),
not this card, and there is no supervision-free restriction (and no
applyRestrictionDictionary: helper in-repo) that hides it. The card is produced
by the pure-Swift provider _TtC11SettingsApp35PrimaryAppleAccountListItemProvider,
bound to account availability, not to a restriction. The one lever that cleanly
removes the row (verified durable across reboot) is the existing Preferences
binary patch cfw patch-settings-root-rows (patch system-preferences-exp-root_rows,
CustomFirmwareSettingsRows.swift), which forces that provider Optional empty —
the Settings-row work-stream, not this dylib.
Store / Game Center hooks — verified present. On signintest-iphone27,
os_log confirmed the hooks install in the relevant processes:
+[AMSAuthenticationViewServiceLauncher launchWithClientInfo:action:xpcEndpoint:]
in Settings, Music and Games; and both Game Center seams
(-[GKLocalPlayer authenticationShowSignInUIForLocalPlayer:origin:dismiss:] and
-[_TtC12GameCenterUI34LocalPlayerAuthenticationPresenter authenticationShowSignInUIForLocalPlayer:origin:dismiss:])
in the Games app. The interactive store/Game-Center sheets could not be
force-triggered on a signed-out, content-less guest (the apps do silent
background token auth and sit in onboarding), but the no-ops are confirmed
in place and will fire when those methods are invoked. libsigninfix emits one
installed … os_log line per hook per process for this observability.
What this does and does not do: it makes the sign-in flows inert guest-wide
(one small dylib). It does not remove the entry banners — see below. There is
no shared "sign-in unavailable" flag to flip to hide banners: they render
because no account exists, and faking an account is disallowed (AuthKit's
shieldSignInOrCreateFlows is age/teen-attestation-specific, not a device "no
sign-in" switch, and triggers its own replacement UI).
Deploy on a sealed-APFS guest: env.install/the fast loop live-writes
/usr/lib, but new launches load the sealed snapshot and a reboot reverts the
live write — so an existing-lib update does not take that way. Use
vphone-launchpad-cli bundle install-local <bundle> --keep-default then
vphone-launchpad-cli vm set-bundle <vm> <version> --update-environment (VM
stopped), which bakes the dylib into the snapshot through the helper.
Sign-in ENTRY (banner/cell) removal — per-app, uneven
Net for entries: 8 apps handled by removal; 2 (Settings card, Game Center) by the sheet hook / Settings patch; Wallet/Camera have none; the real per-app work is 2 easy ObjC swizzles (Mail, Books) and 7 SwiftUI binary patches, of which Find My and Fitness are the only hard launch gates (do these first if the SwiftUI route is pursued).
VM-unavailable features → Settings rows
The lever is the device tree, not libmisfix. libmisfix is not injected
into Settings, and dyld __interpose does not reach MobileGestalt calls made
from inside the shared cache (where the SettingsApp providers run). Instead,
the guest's device-tree /product leaves hardware properties as syscfg/xxxx
placeholders, MobileGestalt reads a placeholder as absent, and Settings'
gestalt visibility rules drop the row for free. This already works and is
maintained by the cfw device-tree patchers (DeviceTreeGuestDevicePatches.swift,
CustomFirmwarePostRestoreDeviceTree.swift). Settings' rule kinds (from
Preferences.bin SettingsSidebarListItemVisibilityRule): gestalt,
biometricKitIsUnavailable, restrictedByManagedConfigurationFeature,
applicationRestricted, featureFlag, boolean, pluginExists, sharedPad,
personaEnrollmentIsDisabled.
Guest hardware (live device tree): present = bluetooth, sep, isp/ispRtb;
absent = baseband/modem, NFC/stockholm, UWB/Rose, wifi-chipset, biometric
(Pearl/Mesa).
Clean wins (no work): cellular, hotspot, carrier, satellite, biometric content — already auto-hidden by the device-tree capability-absent mechanism; just keep the device-tree patcher from ever filling those hardware caps. Patch-free next tier: Bluetooth / Camera / AirDrop via ManagedConfiguration restrictions. Binary-patch tier (other work-stream): the account/service rows and AirPlay/CarPlay.
Recommended plan
- Ship one guest "sign-in suppressor" dylib (SystemHook
dlopen, ObjC swizzle) covering both shared sheet paths — AuthKit/AppleAccountUI and AMSUI/StoreKitUI, plus GameCenterUI. Highest value, lowest cost: every Apple-ID / iCloud / store sign-in flow becomes inert guest-wide. Also fold in the two easy ObjC entry hooks (MailOBWelcomeController/AccountsUI, BooksBK*controllers) since they live in the same dylib pattern. - Keep removing the removable apps (already done) — the cheapest entry hide for App Store, TV, FaceTime, Messages, News, iTunes Store, Camera, Home.
- VM-feature rows: rely on the existing device-tree capability-absent mechanism (already hides cellular/hotspot/satellite/biometric). Add ManagedConfiguration restrictions for Bluetooth / Camera / AirDrop (patch-free). Leave Wi-Fi/Ethernet/Emergency SOS/Passcode.
- SwiftUI entry banners + account/service Settings rows (Find My, Fitness,
Freeform, Music, Photos, Health, Podcasts; Apple Account / iCloud / Wallet /
Game Center / Software Update) need binary patches — expensive, and the
Settings-row half is the separate
settings_app_row_hiding.mdwork-stream (now unblocked: Ghidra is installed). Prioritise Find My + Fitness (hard launch gates) if the app-side SwiftUI patching is pursued.
Artifacts / cross-refs
Scratch (read-only research outputs): /private/tmp/signin-a1/ (AuthKit /
AuthKitUI / AppleAccountUI / Accounts class dumps + disass), /private/tmp/signin-a2/
(per-app binary classification), /private/tmp/vmfeat-b/ (ioreg + decoded MG
cache). Settings-row binary-patch work (separate session): settings_app_row_hiding.md,
Research/Guest/2026-10-07_reverse-settings-rows-report.md, /private/tmp/prefs-audit/.
Device-tree capability mechanism: Research/0_binary_patch_comparison.md
§"An iPhone guest's product description from D47AP".