Blocking OTA software updates on a guest
How to stop a vphone guest from ever checking for, downloading, or installing
an iOS OTA update — the update that would try to replace the research firmware.
Measured live on 2026-10-06 on panetest-iphone (iPhone17,3 iOS 27.0 (24A435),
cloudOS 26.4, bundle 2.6.0-local.92448989), driven over vphoned
(vphone-launchpad-cli guest rpc <name> …).
TL;DR
Disable the three software-update launch daemons with services.disable
(persists in the data-volume launchd override DB) and reboot. After the reboot
none of them load, and Settings › General › Software Update shows "Unable to
Check for Update." No binary patch, no system-volume write.
Leave com.apple.mobileassetd enabled — it serves many non-OTA assets
(dictionaries, keyboards, Siri, fonts); disabling it breaks unrelated features.
Leave akd alone.
On iOS 27 this holds on a guest that presents its board (every device but the
iPhone17,3) only with system-jetsamproperties-cfw-disable_allowed: the
board's jetsam properties mark mobile.softwareupdated and OTATaskingAgent
DisableAllowed = false, and launchd then loads them whatever the override
says. See service trimming.
The daemons and why these three
services.list on the guest, filtered to update machinery, shows the OTA set
running on a fresh boot:
softwareupdatedis the engine that checks for and downloads the OTA, and posts the "update available" badge.softwareupdateservicesdis what the Settings pane and MDM drive to scan.OTATaskingAgentschedules the background automatic-update work.
Disabling those three removes both the automatic path and the manual scan. The
Settings pane launches softwareupdateservicesd on demand; with it disabled
launchd will not start it, so the pane errors instead of scanning — the desired
"blocked" surface.
The recipe (measured)
services.disable → launchctl disable, which writes the override to
/var/db/com.apple.xpc.launchd/ on the data volume — writable and durable,
no system-volume (sealed) write needed. services.disabled reads the three back
as true.
services.stop does not work here and is not needed: it returns
launchd status 144 ("stop requires launchctl service-configure privilege"),
and so do unload / remove / start. The running daemons therefore keep
running (and KeepAlive-respawn after a processes.kill) for the rest of the
current boot; the reboot is the clean stop. disable itself does not hit
the 144 wall.
What was measured
"Automatic Updates" in that pane may still read On — it is a stored preference, inert once the daemons that would act on it are gone.
Endpoint blackhole (optional hardening — not required, and not trivial here)
With the daemons disabled, nothing contacts Apple's update catalog, so a DNS block is belt-and-suspenders only. Two obvious routes are both closed on this guest:
/etc/hosts:files.writefails witherrno 30 Read-only file system./etc→/private/etclives on the sealed root volume; writing it needs themount -u -w /remount path (environment.installuses it) and may not persist. Not done.network.static_names(LocalOnly A records via mDNSResponder): usable, butvphone-vmcallsnetwork.static_names.setwith the Mac's.localname after every connect, which replaces the table — so a gdmf/mesu entry put there does not survive the next connect. Not durable.
If a catalog block is ever wanted anyway, the hosts the OTA scan uses are
gdmf.apple.com (the modern Pallas catalog) and mesu.apple.com (legacy), plus
the download CDNs appldnld.apple.com / updates-http.cdn-apple.com /
updates.cdn-apple.com. Do not blackhole gs.apple.com — that is the
TSS / activation signing server. Given the daemon disable fully blocks OTA, the
endpoint block is left undone.
Revert
Productizing
The disable is three idempotent services.disable calls that survive reboot —
a natural vphone-cli/vphoned convenience verb (e.g. cfw block-ota or a
vphoned system.block_ota) could apply and revert them in one call, and a
machine config.plist flag could reassert them at boot the way
unlocksAtStartup drives screen.unlock. Not built yet.