Trimming system services
vphoned's trimmed service profile turns off about 140 launchd jobs a
research VM has no use for: Siri and the intelligence daemons, Find My,
Wallet/NFC/Secure Element, Watch, Home, health coaching, Maps background
work, iCloud and iMessage sync, ads and recommendations, telemetry, accessory
firmware updates and OTA. On an iOS 27.0 guest that cuts process memory by
29%, idle host CPU by 26% and idle host writes by 60%; on iOS 26.6.2, with
the same list less what 26 does not load, by 30%, 39% and 94%. No vphone
feature was lost on either. The profile is applied once, in the setup boot
of a machine template, and every clone of the template inherits it.
services.profile and services.profile.apply are described in
vphoned_http_api.md; the lists are in
VPhoneDaemon/Daemon/GuestServiceProfile.swift.
How it works
The profile uses the override launchctl disable writes, which lives in
launchd's database on the data volume
(/private/var/db/com.apple.xpc.launchd/disabled.plist). launchd does not
load a disabled job at the next boot. It is the same mechanism
services.disable and the OTA block use.
- It takes a reboot.
services.stopandservices.unloadanswer with launchd status 144 on the guest, so a running job keeps running until the guest restarts. - It is not a firmware patch. The system volume and
launchd.plistare untouched, nothing goes into a patch set, and the Services panel still lists every job, disabled ones marked as such. - On iOS 27 it leans on one. launchd there ignores the override for the
jobs the board's jetsam properties mark
DisableAllowed = false, andsystem-jetsamproperties-cfw-disable_allowedremoves that mark (iOS 27 and DisableAllowed). - It reverts cleanly. vphoned records the labels it disabled in
/var/db/vphoned/service-profile.plist.profile: "none"turns back on those labels and no others. A job that was already disabled when the profile ran, such as the OTA trio, is never recorded and stays disabled. - The host has to ask. The host cannot write the data volume of a guest: it is encrypted with the guest's keys. So the profile is applied by vphoned from inside the guest.
What trimmed disables
The base group, by area:
siriactionsd stays (list version 2, 2026-10-24). It was in the measured
base list, but it also serves com.apple.siri.VoiceShortcuts.xpc, through
which every Control Center control and App Intents widget serializes its
intent (-[INAppIntent initWithAppBundleIdentifier:linkAction:linkActionMetadata:]).
With it disabled, each such extension traps in WidgetKit
(ControlAction.swift: Fatal error: Can't create CHSIntentReference from BarcodeScannerIntent: nilError), and Control Center draws Timer, Calculator,
Camera, Code Scanner, Alarm, Magnifier and Voice Memo as blank circles. Seen
on an iPhone Duo 27.1 guest; allowing the label and restarting restored every
control. It is now in neverDisable, so trimmed no longer selects it and a
guest that applies the profile again turns it back on.
passd and financed stay (list version 3, 2026-10-24). Wallet's first
screen asks passd synchronously for its pass groups
(-[PKPassLibrary _getGroupsControllerSnapshotWithOptions:synchronously:retries:handler:]),
and passd waits on financed's Core Data store
(com.apple.financed.service.coredatastore). With passd disabled, Wallet's
NSXPCStore retried until FrontBoard's 20-second scene-create watchdog
killed it; with passd running but financed disabled, the synchronous call never
returned and the same watchdog fired. With both running Wallet opens; passd
keeps retrying nfcd, which stays disabled, at about 0.02 CPU-seconds a minute.
generativeexperiencesd stays (list version 4, 2026-11-02). It serves
com.apple.generativeexperiences.ExternalProviderService, and with it
disabled BackgroundShortcutRunner reconnects to that service with no back-off
(EPS failed to resubscribe to provider changes, NSCocoaErrorDomain 4099,
after each failed bootstrap look-up), while launchd answers every look-up. On
three idle iPhone17,3 27.0.1 guests built trimmed+accounts,
BackgroundShortcutRunner used about 9 CPU-seconds and launchd about 4.5 in a
12-second window, and each VM held the host at about 130% CPU. With the label
enabled and the guest restarted, the whole guest used 0.3 to 0.5 CPU-seconds
per 10 seconds, the VM 17% or less, and generativeexperiencesd stayed quiet
(#791). It is now in
neverDisable, on 26 as well as 27.
Some of these are only optional: Shortcuts, dictation, HomeKit, Maps,
Mail, Reminders, iMessage and Safari bookmark sync. A research target that
uses one of those frameworks needs its daemon back. Pass that label in allow
and apply again; the profile turns back on a recorded label it no longer
selects.
iOS 26 and 27 have a list, both measured on iPhone guests (see iOS 26 for how the 26 list differs). vphoned picks the list by the major version alone, so an iPadOS guest of those versions gets the iPhone list. An iPad16,1 template builds with it on 26.6.2 and on 27.0.1 (iOS 27 and DisableAllowed); its idle cost was not measured there. Any other version gets "No trimmed service list".
Never disabled
GuestServiceProfile.neverDisable lists what the profile refuses to disable,
whatever list or group names it. Five kinds of job were measured to make the
guest worse when off, and are kept out of the lists for that reason:
The rest of the list is what vphone's own features and the tools it drives
rely on. Each one was traced to vphoned, IcliKit, the guest hooks or a
research note: SpringBoard, backboardd, runningboardd, lsd,
containermanagerd, installd, misagent, lockdownd, remoted, cryptexd,
remotepairingdeviced, avconferenced, diskimagesiod, locationd, bluetoothd
(locationd calls it synchronously), pasted, powerd, amfid
(com.apple.MobileFileIntegrity), profiled, keybagd, cfprefsd, notifyd,
mDNSResponder, configd, timed, tzlinkd, securityd, logd, diagnosticd,
ReportCrash, osanalyticshelper, audiomxd, mediaplaybackd, mediaremoted,
cameracaptured, mobileactivationd, mobileassetd and storekitd (which the OTA
and App Store notes require), and vphoned itself. dtremotedisplayd and
dtdeviceinfod are not launchd.plist jobs on 27.0 and are out of reach anyway.
Two jobs looked like candidates and cannot be disabled this way.
com.apple.siri.context.service and com.apple.siri.acousticsignature are
XPC services, not LaunchDaemons, so the override is written and they run
anyway.
Measurements
These numbers come from svctest-a, a --new-identity clone of an iPhone
27.0 (24A435) guest with cloudOS 26.4, 8 GB and 8 cores, started --headless
after setup.skip (2026-10-07). Both runs used the same machine and disk. The
baseline is its third untrimmed boot. Host figures are proc_pid_rusage of
the VM process every 10 s. Guest figures come from processes.list,
memory.jetsam and /usr/bin/taskinfo, run through a one-shot launchd job.
Boot CPU varied more between runs (72–208 s in the first 100 s untrimmed, 44–138 s trimmed) than trimming changed it, so only the drop in boot writes is dependable. vphoned answered 5–14 s after start either way, because launchd starts it early. The host keeps most of the memory the guest frees, since the VM has no balloon device. The gain is headroom inside the guest.
A template set up with the profile (P0, 2026-10-07) showed the same effect on its clones. A clone's first 100 s wrote 372–375 MiB, against 1040 MiB for a clone of an unprepared template.
Function checks
These checks gave the same result trimmed and untrimmed: unlock, screenshot,
apps.list (258), installing a small app with apps.install and launching it,
Settings, clipboard, audio.state, starting the Camera app, uninstalling.
SpringBoard kept the same pid through every run, and no new crash type
appeared.
Not checked:
- installd's own install path (
ideviceinstaller). - Xcode and DeviceHub screen viewing. The jobs they need are kept.
- Frida.
- A real microphone path.
- Location. 27.0's locationd hangs or crashes with
EXC_ARM_PAC_FAILon untrimmed guests too, and Location Services were off aftersetup.skip. On iOS 26.6.2, where locationd works, location simulation and Maps' blue dot work with the Maps background group andnearbydoff (iOS 26). On 27 it waits for locationd to be fixed.
Known side effect: with weatherd off, the default Home Screen's Weather
widget reads "Weather Unavailable". Pass allow: ["com.apple.weatherd"] to
keep it.
iOS 26
The iOS 26 list is the iOS 27 one mapped onto iPhone17,3 26.6.2 (23G90),
measured on t26-a (cloudOS 26.4, 8 GB, 8 cores, --headless, 2026-10-09),
a clone of a template built --service-profile none.
How it was derived:
- The LaunchDaemons and LaunchAngels of both IPSWs'
launchd.plist, byLabel: 685 on 26.6.2, 724 on 27.0. - Of the 137 labels of the 27 base list, 132 exist on 26 under the same
name.
cloudtelemetryd,hybridsearchd,libsqlite3.dbtelemetryd,speechmaintenancedandvisualintelligenceddo not exist on 26, under that name or another. - Of those 132, 131 load on the guest.
safetyalertsdhas aLimitLoadToHardwarelist of real devices on 26, so it never loads there and is left out, as the 27 list holds only jobs loaded at boot. - The store, sign-in follow-up and account groups have the same seven labels on 26.
- Four jobs exist only on 26.
com.apple.factory.NFQRCodedlives in/Developerand does not load;deviceinterfacedandsystemactionsload but did not run.com.apple.timesync.audioclocksyncdruns and was the guest's largest idle CPU user once trimmed (1.5 s and 14,000 context switches per 15 minutes, printing PTP clock statistics). With it disabled as well the host's idle CPU did not move (2.69% of a core either way), so it stays on.
So trimmed on 26 disables 131 + 2 + 2 = 135 labels (GuestServiceProfile.base26,
notLoadedOn26). The never-disabled set needed nothing new: no job of the
list made the guest worse when off, and the traps of 27 are kept on 26 as
on 27 without being measured again.
One untrimmed and one trimmed run of the same machine and disk, the windows from 120 to 1020 s after start. Boot CPU was 72 s untrimmed and 48–75 s trimmed in the first 100 s, within the spread 27 showed. Most of the untrimmed idle writes came in one burst five minutes after boot. With the profile on, the screen still turned off, the logs stayed at 50–80 lines per 10 s after two minutes, and locationd started once on every boot.
Function checks, trimmed and untrimmed alike: unlock, screenshot,
apps.list (236), installing a small app with apps.install, launching it,
Settings, clipboard, audio.state, playing a WAV in Safari (21 s, 247
writes, none starved, host 47999.7/s), starting the Camera app (26.6.2
publishes the synthetic camera: libvcamcaptured appends its source to
_sSourceList), location.set and location.current with Location
Services turned on, and Maps showing the blue dot at the simulated
coordinate, uninstalling. SpringBoard kept its pid throughout.
One difference showed in crash reports. On 2 of 16 trimmed boots, and none
of 7 untrimmed ones, three control and widget extensions (CalculatorWidget,
BarcodeScannerWidgetExtension, LauncherControlExtension) stopped in their
first seconds in WidgetKit's ControlAction.init(_:) with "Can't create
CHSIntentReference from" an App Intent. They are launched again on demand,
and nothing visible was affected; which disabled job the intent reference
needs was not found. SiriSearchFeedback crashed in both configurations.
iOS 27 and DisableAllowed
On a guest that presents its board, iOS 27 kept five of the profile's jobs running after the reboot, and a template's strict setup boot failed its last check (2026-10-13, iPad16,1 27.0.1 (24A446), bundle 2.9.3):
All 144 overrides were in place (services.profile → disabled). 139 of
those jobs were not loaded at all; services.print showed the other five with
force-enabled among their properties.
The cause is a key iOS 27 added to the jetsam properties,
/System/Library/LaunchDaemons/com.apple.jetsamproperties.<target-type>.plist.
Under Version4 → Daemon (and XPCService) → Override → <label>, 19
entries carry DisableAllowed = false: the five above and
com.apple.DumpPanic, ReportCrash, ReportCrash.Root, ReportCrashService,
logd, logd_helper, logd_reporter, mobile.obliteration,
mobileactivationd, osanalytics.osanalyticshelper and its .agent,
pluginkit.pkd, sysdiagnose and sysdiagnose_helper. launchd reads the key
with the other JetsamProperties and, when it bootstraps a disabled job that
has it, logs Ignoring disabled state because DisableAllowed=false and loads
the job anyway. launchctl print then lists force-enabled.
Which file launchd reads follows the device tree's target-type:
- An iPhone17,3 guest keeps
VPHONE600. The profile and the OTA block were measured there, and a slimmed 27.0.1 template passes the same check without the patch, so nothing is force-enabled; the image presumably has no jetsam properties under that name, which was not looked at. - Every other guest presents its board (
VPhoneGuestDevice.presentsBoard: J410 for the iPad16,1), gets that board's file, and with it the 19 marks. - iOS 26 has the files without the key: the J410 one on 26.6.2 (23G90)
holds no
DisableAllowed, andservices.dumpon that guest shows noforce-enabledjob. A trimmed iPad16,1 template builds there unchanged.
system-jetsamproperties-cfw-disable_allowed removes every
DisableAllowed = false from each com.apple.jetsamproperties.*.plist during
cfw install and cfw update-environment, for iOS 27 only. It keeps a .bak
beside each file and puts it back when the patch is turned off. Nothing else
in the files changes, and the key has no effect on a job that is not disabled.
Two things did not work from inside the guest. The system volume is read-only
there, so vphoned cannot edit the file. And services.remove unloads the
three of the five that live in the user domain but is refused for
com.apple.analyticsd and com.apple.OTATaskingAgent, which are in the
system domain ("remove failed: launchd status 3 (No such process)").
Validated on 2026-10-13 with the patch, on iPad16,1 27.0.1 (24A446):
- The failing guest after
cfw update-environment("19 DisableAllowed = false removed" for each of J410 and J411): noforce-enabledjob, none of the 144 labels loaded,services.profile→runningempty. - A new machine from nothing (
vphone-launchpad-cli vm create --device iPad16,1 --slim on): every step passed, the strict setup boot in 87 s (settled after 64 s, 144 services owned, verify passed), and the clone of template0c8682d7c241reaches the Home Screen with the same three answers.
The same creation on iPadOS 26.6.2 (23G90) passed with bundle 2.9.3, which
does not have the patch: 138 services owned, setup boot in 90 s, template
65cb509bf161.
An iOS 27 guest restored by an older bundle has the marks until an environment
update or a reinstall runs with the patch. Until then the five jobs keep
running, and so does softwareupdated on a guest whose OTA block was applied
by hand.
Applying it in a template
A template is changed once, in its setup boot, and frozen; every clone inherits the overrides and the record. The order matters:
setup.skip.setup.settle, until first-boot app expansion is done.- Remove the system apps.
services.profile.apply {profile: "trimmed", force: true}. Thesignin_followupgroup belongs after the first-boot work, which step 2 already waited for. Every override waits for the reboot anyway, so one call covers both the services and the sign-in follow-up.- Reboot, and check that no job the profile owns is running
(
services.profile→runningempty). - Shut down and freeze.
Reverting
On a running guest:
That turns back on only what the profile disabled. To keep the profile but
bring back one job, apply trimmed again with that label in allow. To
bring back the Apple Account daemons on a guest that had accounts, apply
trimmed without that group.
Idle cost of vphone's own components
With the profile applied, the largest idle cost left in a guest was vphone's
own plumbing: timers in the guest hooks that ran whether or not anything used
the feature behind them. Measured 2026-10-09 on two clones of one trimmed
iPhone 27.0 (24A435) template, --headless, 15 idle minutes (902 s) starting
three minutes after boot, screen off and locked, no camera, audio or API
client. Guest figures are /usr/bin/taskinfo differences (CPU time and
interrupt wakeups per process), run through a one-shot launchd job; host
figures are the CPU time of the VM's Virtualization service and of
vphone-vm.
What is left at the top is Apple's own: mDNSResponder (about 1,900), locationd (1,100), cameracaptured and audiomxd (about 950 each with nothing of ours running in them), clipserviced, fseventsd and backboardd.
remotepairingdeviced is quiet on a guest that was never paired: 30 wakeups and under 0.01 s of CPU per 15 minutes, 0.12 s in its first 18 minutes, no log lines in a minute. No vphone code talks to it. The 3.5–8 s measured earlier came from a clone of a DeviceHub test machine that had been paired with this Mac's CoreDevice, so that cost follows pairing between Apple's daemons on both sides. An unpaired guest that is unlocked gets a "Trust This Computer?" alert from the Mac shortly after; answering it is what pairs it.
Checked after the change: playback through Safari (91 s, 1,073 writes, none
starved, host rate 48000.1/s) on a boot whose speaker had already started and
stopped once, so the timer was re-armed; audiomxd fell back to 1.6 wakeups a
second when it stopped. The gyroscope configuration still reaches the provider
and provider_running stays true while idle. device.state arrives about a
second after an event socket opens and again after an unlock; vphoned took 45
wakeups a minute with a socket open and 7 without. A guest with an older
vphoned was still updated by vphone-vm over HTTP. Not checked live: camera
frames, because the iPhone 27.0 guest publishes no synthetic camera
(libvcamcaptured logs _sSourceList not located on every boot, before and
after), so no client can reach the drive; and the microphone, whose code did
not change.
Open items
- iPadOS. vphoned picks the list by major version only, so iPad guests on 26 and 27 get the iPhone lists. Templates build with them (iOS 27 and DisableAllowed), but no iPad guest's idle cost or function checks have been measured.
- Other board-presenting iPhones on iOS 27. They read their board's jetsam properties as an iPad does, so the same patch should apply; only the iPad16,1 was run.
- WidgetKit control extensions on iOS 26. The
ControlAction.init(_:)crashes on 2 of 16 trimmed 26.6.2 boots (iOS 26) point at a disabled job an App Intent reference needs; which one was not found. - Location on iOS 27. Location simulation under the profile is verified on 26.6.2 only; on 27.0 it waits for locationd itself to work (locationd startup deadlock).
- Host memory. The VM has no balloon device, so the guest's freed memory
stays with the VM process (8.67 → 7.26 GB on 27.0). Returning it would take
VZVirtioTraditionalMemoryBalloonDeviceand a guest that drives it, which was not evaluated; giving trimmed machines less memory is the simple way to use the headroom. - Sample size. Each configuration's idle figures come from one or two
15-minute windows, and bursts of maintenance work that
dasdschedules (mediaanalysisd, cloudd, searchd) dominated some of them. They show the direction, not a precise rate.