Companion user-mode patches to the kernel-side OID rename. Mangles
byte 5 of every `kern.hv_vmm_present` cstring inside DSC dylibs EXCEPT
those in `DONT_PATCH_INSTALL_NAMES` (sign-in / device-likeness consumers,
~15 entries). Patched dylibs query the renamed OID and get the truthful
1 (graphics + accel passthrough); blacklisted dylibs keep the original
cstring, hit ENOENT on the renamed kernel, defensively cache 0 ("not
running on a VM") for sign-in / device-attestation surfaces.
- `scripts/patchers/cfw_patch_hv_vmm_dsc.py` — DSC orchestrator. Walks
every `kern.hv_vmm_present\\0` cstring in any executable mapping,
resolves the containing dylib via Mach-O-header walk-back +
LC_ID_DYLIB, applies the byte-5 mangle to non-blacklisted dylibs, and
drives slot-hash re-attestation in the chunk's `CS_CodeDirectory`.
- `scripts/patchers/cfw_dsc_chunks.py` — pure-Python helper for the
chunked DSC layout: vmaddr<->chunk-fileoff mapping, install-name
walk-back, byte-level read/write at a vmaddr.
- `scripts/patchers/cfw_dsc_codesign.py` — per-page SHA-256 slot-hash
re-attestation for DSC chunks (16 KiB pages). Required on
`codeSigningMonitor == 2` (TXM) hardware where per-page hash checks
would otherwise SIGKILL the patched dylibs at first demand-page-in.
- `scripts/patchers/cfw_patch_hv_vmm.py` — standalone Mach-O variant of
the cstring mangle (kept for completeness; the historical
standalone-binary loop step was removed in favor of the
blacklist-flip-via-kernel-rename design).
- `scripts/patchers/cfw_patch_hv_vmm_rootfs.py` — rootfs-path inventory
shared with the install scripts (former JB-3.5 / 6.5/7 loop input).
- `scripts/patch_hv_vmm_userland.sh` — thin wrapper used by the install
script (`dsc` and `standalone` operations; `watchdogd` is added by
the next commit).
- `scripts/patchers/cfw.py` — new subcommands: `patch-hv-vmm`,
`patch-hv-vmm-dsc`, `list-hv-vmm-rootfs-paths`.
- `scripts/cfw_install_exp.sh` — adds a pre-step that decrypts the
SystemOS Cryptex AEA into the cache location `cfw_install.sh` already
uses, mounts it, applies the DSC patch, and unmounts. The unmodified
base `cfw_install.sh` then picks up the cached (patched) DMG.
Research docs in this commit describe the full EXP variant comprehensively
(`research/0_binary_patch_comparison.md` top-of-doc note, EXP-Only
Kernel Methods section, DSC userland subsection, plus forward references
to EXP-JB-3.5 / EXP-JB-6 / EXP-JB-7 subsections wired up by the following
commits; `research/firmware_manifest_and_origins.md` sections 9-11
similarly forward-describe the DT and build-version pieces).
JB and DEV variants are NOT affected: their install scripts don't invoke
any of this.
33 KiB
kern.hv_vmm_present user-mode call sites — iPhone17,3 26.1 (23B85)
Source material: ipsws/iPhone17,3_26.1_23B85_Restore (raw IPSW; encrypted DMGs)
and ipsws/iPhone17,3_26.1_23B85_Restore_extracted (rebuilt rootfs with split DSC).
This document enumerates every place in user space that reads
kern.hv_vmm_present via the sysctl family on this firmware, what each
caller appears to do with the result, and what is reasonable to expect from
patching that specific reader instead of the kernel.
Current shipping design — blacklist-flip + kernel rename
The current patch pipeline takes a kernel-rename + user-mode-blacklist approach. Summary:
- The Swift kernel patch
patch_hv_vmm_rename(JB-26, Group B) renames the kernel'shv_vmm_presentsysctl OID name in place toXv_vmm_present(single byte change at offset 0 of the 14-byte cstring\0hv_vmm_present\0). After this kernel patch:sysctlbyname("kern.hv_vmm_present", ...)returns ENOENT.sysctlbyname("kern.Xv_vmm_present", ...)returns 1, the OID's real int value.
- The user-mode mangle pattern moved from byte 0 (
'k' → 'X', producingXern.hv_vmm_present— unroutable becauseXernisn't a top-level namespace) to byte 5 ('h' → 'X', producingkern.Xv_vmm_present— keepskern.intact, routes to the renamed OID). scripts/patchers/cfw_patch_hv_vmm_dsc.pynow applies the mangle as a BLACKLIST. The listDONT_PATCH_INSTALL_NAMESnames dylibs whose cstring is LEFT untouched — they query the original (now ENOENT-returning) name and cache 0 ("not in a VM"). Every other DSC dylib with the cstring gets the byte-5 mangle, queries the new name, and caches 1 ("in a VM", same as stock) — keeping graphics + compute/accel fast paths intact.- The standalone-binary patch step over SSH (originally
cfw_install_jb.shJB-3.5 andcfw_install_dev.sh6.5/7) was removed. With the kernel rename in place, those 6 rootfs binaries fall into the "unpatched → ENOENT → cache 0" bucket automatically.
The earlier whitelist-only design (no kernel patch, mangle a chosen subset to ENOENT) is preserved for reference in the section "Pre-blacklist whitelist enumeration" below, since the per-caller classification work is still the source-of-truth for what each binary's cstring usage actually looks like.
What the sysctl is
kern.hv_vmm_present is defined in XNU at
bsd/kern/hvg_sysctl.c:176:
static SYSCTL_PROC(_kern, OID_AUTO, hv_vmm_present,CTLTYPE_INT | CTLFLAG_ANYBODY | CTLFLAG_KERN | CTLFLAG_LOCKED, …)
Backing handler sysctl_vmm_present (line 136 in the same file) returns
an int, computed on arm64 from IODTGetDefault("vmm-present", …) —
i.e. a device-tree key that Apple's Virtualization.framework /
PCC bring-up plants in the guest. On real hardware the key is absent
and the read returns 0; on PCC/research VMs it returns 1. That is
exactly the path you are flipping today by patching the kernel.
Because this is a single named OID with CTLFLAG_ANYBODY, every user-mode
reader funnels through the canonical sysctlbyname() shape:
adrp x0, <page> ; x0 = "kern.hv_vmm_present"
add x0, x0, #<off>
sub x1, x29, #4 ; x1 = &result (int)
mov x2, sp ; x2 = &len (= 4)
mov x3, #0 ; newp
mov x4, #0 ; newlen
bl _sysctlbyname
This shape is the matcher I used to flag “real readers” below.
Method
- Located the literal
"kern.hv_vmm_present\0"inside every Mach-O of the rebuilt rootfs (the split DSC dylibs and every executable). 56 binaries matched on bytes alone. - For each match, parsed the LC_SEGMENT_64 layout and located the
actual section. Anything that wasn't in
__TEXT,__cstring(or another section flaggedS_CSTRING_LITERALS, flag0x2) was investigated separately. - For each cstring hit, looked for ADRP+ADD or ADRP+LDR pairs in any
executable section that compute the string's vm address, then walked
forward up to ~24 instructions tracking simple register propagation
(
mov xN, xM). - Classified each xref:
CANONICAL— string ends up inx0,x3=x4=0, BL within the window. Matchessysctlbyname(name, oldp, oldlenp, NULL, 0).PROBABLE— string inx0and a BL in window, butx3/x4zeroing not seen.REFERENCED— xref exists but it is loaded into a non-x0 register or used as an address anchor for adjacent data, not handed to a syscall.STORED— string written into a struct (i.e. a registry, not a direct caller).DEAD— string lives in__cstringbut no executable instruction points at its page.
The full per-binary results (snippets, addresses, classification) are
saved at outputs/hv_vmm_present_xref.json.
Inventory by class
A. CANONICAL sysctlbyname("kern.hv_vmm_present", …) callers — 35
Each line is <binary> string@<vmaddr> xref@<vmaddr>. The xref
address is the add instruction that finalizes the pointer value
in x0; immediately followed by the canonical 5-arg sysctlbyname
preamble and a bl.
usr/lib/libMobileGestalt.dylib string@0x1b0201c60 xref@0x1b0198d00
System/Library/Frameworks/CoreVideo.framework/CoreVideo string@0x19d17a4bb xref@0x19d122eb8
System/Library/Frameworks/CoreML.framework/CoreML string@0x1950ed0d0 xref@0x194c3c754
System/Library/Frameworks/StoreKit.framework/Support/storekitd string@0x10031a890 xref@0x100176f6c
System/Library/PrivateFrameworks/CoreRE.framework/CoreRE string@0x1e3171b20 xref@0x1e2790f28
System/Library/PrivateFrameworks/Espresso.framework/Espresso string@0x1949204e5 xref@0x193bef83c
System/Library/PrivateFrameworks/AppleNeuralEngine.framework/AppleNeuralEngine string@0x1ad297375 xref@0x1ad248248
System/Library/PrivateFrameworks/AppStoreDaemon.framework/Support/appstored string@0x10046dae6 xref@0x1003e25b8
System/Library/PrivateFrameworks/AppStoreUtilities.framework/AppStoreUtilities string@0x23ff916d7 xref@0x23ff8bfe4
System/Library/PrivateFrameworks/ApplePushService.framework/ApplePushService string@0x1b235fd50 xref@0x1b2358d10
System/Library/PrivateFrameworks/AuthKit.framework/AuthKit string@0x1933c11de xref@0x19322f7e8
System/Library/PrivateFrameworks/AAAFoundation.framework/AAAFoundation string@0x1c86581a1 xref@0x1c8650ee4
System/Library/PrivateFrameworks/AirPlaySupport.framework/AirPlaySupport string@0x2223618a0 xref@0x222306a28
System/Library/PrivateFrameworks/CorePrescription.framework/CorePrescription string@0x2471fe6ed xref@0x2471ebc18
System/Library/PrivateFrameworks/CorePrescription.framework/XPCServices/CorePrescriptionService.xpc/CorePrescriptionService string@0x100092175 xref@0x100005064
System/Library/PrivateFrameworks/CoreCDP.framework/CoreCDP string@0x1dedfa62d xref@0x1dedbb724
System/Library/PrivateFrameworks/DVTInstrumentsUtilities.framework/DVTInstrumentsUtilities string@0x247f42ad0 xref@0x247f1154c
System/Library/PrivateFrameworks/DeviceCheckInternal.framework/DeviceCheckInternal string@0x2487a018c xref@0x24878d10c
System/Library/PrivateFrameworks/DeviceIdentity.framework/DeviceIdentity string@0x2260a4b25 xref@0x2260862a4
System/Library/PrivateFrameworks/EmailFoundation.framework/EmailFoundation string@0x1c61cd1f7 xref@0x1c6165e88
System/Library/PrivateFrameworks/FindMyBase.framework/FindMyBase string@0x1df635420 xref@0x1df5fb8d4
System/Library/PrivateFrameworks/IDSFoundation.framework/IDSFoundation string@0x1a7ea0fe4 xref@0x1a7c65dc4
System/Library/PrivateFrameworks/IOSurfaceAccelerator.framework/IOSurfaceAccelerator string@0x2547cd71c xref@0x2547caa48
System/Library/PrivateFrameworks/MobileActivation.framework/MobileActivation string@0x2598f06c6 xref@0x2598e2618
System/Library/PrivateFrameworks/PhotoFoundation.framework/PhotoFoundation string@0x1d8ba907c xref@0x1d8b9e7fc
System/Library/PrivateFrameworks/RenderBox.framework/RenderBox string@0x195e54555 xref@0x195d344a0
System/Library/PrivateFrameworks/TrialServer.framework/TrialServer string@0x26f32471e xref@0x26f224120
System/Library/PrivateFrameworks/VisionKitCore.framework/VisionKitCore string@0x1b442d9cf xref@0x1b435f0a4
System/Library/PrivateFrameworks/WatchdogServiceManagement.framework/WatchdogServiceManagement string@0x272725b23 xref@0x272725114
System/Library/PrivateFrameworks/WebGPU.framework/WebGPU string@0x225722b6c xref@0x22552c2bc
System/Library/PrivateFrameworks/caulk.framework/caulk string@0x27512204f xref@0x275112808
System/Library/DataClassMigrators/MobileActivationMigrator.migrator/MobileActivationMigrator string@0x72aa xref@0x3024
System/Library/ExtensionKit/Extensions/HostInferenceProviderService.appex/HostInferenceProviderService string@0x1000112b0 xref@0x10000b890
Applications/CheckerBoard.app/CheckerBoard string@0x10006fb56 xref@0x1000192e8
Applications/StoreKitUISceneService.app/StoreKitUISceneService string@0x1000a2ca0 xref@0x100083f04
All 35 readers share the same five-argument shape. Most also share a
post-call idiom: store the boolean result into a static byte (cache),
and a sibling getter() reads the cached byte. libMobileGestalt is the
clearest exemplar:
0x1b0198cf0 stur wzr, [x29, #-4]
0x1b0198cf4 mov w8, #4
0x1b0198cf8 str x8, [sp]
0x1b0198cfc adrp x0, #0x1b0201000
0x1b0198d00 add x0, x0, #0xc60 ; "kern.hv_vmm_present"
0x1b0198d04 sub x1, x29, #4
0x1b0198d08 mov x2, sp
0x1b0198d0c mov x3, #0
0x1b0198d10 mov x4, #0
0x1b0198d14 bl #<sysctlbyname>
0x1b0198d18 cbnz w0, ...skip...
0x1b0198d1c ldur w8, [x29, #-4]
0x1b0198d24 cset w8, ne
0x1b0198d28 adrp x9, #0x1ed446000
0x1b0198d2c strb w8, [x9, #0xcf8] ; cached_is_vmm := (val != 0)
CoreML, CoreRE, Espresso, AppleNeuralEngine, MobileActivation,
caulk, RenderBox, etc. all do the same: one call, cached, sibling
accessor. That makes the patch surface for each one very small —
overwriting cset w8, ne with mov w8, #0 (or NOPing the call and
zeroing the cached byte's accessor) is all that's needed per binary.
B. False positives ruled out by xref analysis
15 of the 56 string-bearing binaries are not actual readers:
B.1 Embedded entitlement plist (the binary just declares it can read the OID)
These four embed a com.apple.security.exception.sysctl.read-only (or
sysctl-read) entitlement that names kern.hv_vmm_present. The
entitlement only authorises the sandbox; the actual sysctlbyname call
happens in libraries they link (almost certainly libMobileGestalt or
CoreFoundation). The binaries themselves contain no instruction
xref to the string.
Applications/Family.app/Family
Applications/PeopleMessageService.app/PeopleMessageService
Applications/PeopleViewService.app/PeopleViewService
System/Library/PrivateFrameworks/DataDetectorsUI.framework/PlugIns/com.apple.DataDetectorsUI.ActionsExtension.appex/com.apple.DataDetectorsUI.ActionsExtension
B.2 Compiled sandbox profile (the string lives inside an embedded SBPL blob)
The string is in __TEXT,__const, surrounded by high-entropy bytes that
match a compiled sandbox profile. No code reaches the string. As with
B.1, these only declare the OID is permitted — not that they read it.
usr/sbin/absd
usr/sbin/fairplayd.H2
System/Library/CoreServices/ClarityBoard.app/ClarityBoard
System/Library/PrivateFrameworks/CoreALD.framework/CoreALD
System/Library/PrivateFrameworks/IDS.framework/identityservicesd.app/identityservicesd
System/Library/PrivateFrameworks/SonicKit.framework/SonicKit
System/Library/PrivateFrameworks/VideosUI.framework/VideosUI
B.3 String present in __cstring but unreferenced (linker DCE leftover)
The string sits in the cstring section but no ADRP+ADD/LDR in any executable section computes its vmaddr, and no 8-byte little-endian encoding of the address (raw, image-relative, or 36-bit chained-fixup shape) appears in any other section either.
usr/sbin/bluetoothd (verified: 0 ADRPs into the page; 0 byte-pointer matches)
System/Library/Frameworks/MediaToolbox.framework/MediaToolbox
System/Library/Frameworks/ManagedAppDistribution.framework/Support/managedappdistributiond
System/Library/PrivateFrameworks/ApplePushService.framework/apsd
System/Library/PrivateFrameworks/CMCapture.framework/CMCapture
System/Library/PrivateFrameworks/MagnifierSupport.framework/MagnifierSupport
System/Library/PrivateFrameworks/NeuralNetworks.framework/NeuralNetworks
System/Library/PrivateFrameworks/Recon3D.framework/Recon3D
System/Library/PrivateFrameworks/VFX.framework/VFX
B.4 Adjacent‑string anchor (REFERENCED but not a sysctl call)
System/Library/Frameworks/SoundAnalysis.framework/SoundAnalysis
The ADRP+ADD here computes x8 = "kern.hv_vmm_present", then immediately
x20 = x8 - 0x20, i.e. it is using our string as a fixed offset to reach
a different sysctl name 32 bytes earlier in the cstring section. The
sysctlbyname call passes x20, not our string. No need to patch.
What each CANONICAL caller is reasonable to expect to do
I am being literal here: I report what the framework path implies and what the disassembly shows. Beyond what's listed I do not have direct evidence of behaviour and would need runtime tracing to be sure.
| Component | Likely role of the check | Patching effect (only this caller) |
|---|---|---|
libMobileGestalt.dylib |
Implements MGCopyAnswer("hv-vmm-present") and similar — the answer most other Apple frameworks query through it. Single shared cache byte. |
Many higher-level callers ask MobileGestalt instead of doing their own sysctl. Forcing MG's cache to 0 makes most “am I a VM?” checks throughout the system see "no" without further code changes. Highest-leverage single patch. |
CoreML.framework / Espresso.framework / AppleNeuralEngine.framework / CoreRE.framework / RenderBox.framework / WebGPU.framework / caulk.framework |
Compute / accel paths. Each independently caches a "running on VM" flag; their respective dispatchers gate ANE/Metal/HW codecs. | Disables their VM-aware fast-path bypasses. Practical effect: lets these libraries try the same code path they'd use on real silicon. On a research VM with PCC PV=3 they may now hit code that demands real ANE/SEP/etc. and fail — patch only if you specifically need them to take the silicon path. |
IOSurfaceAccelerator.framework |
Hardware surface allocation paths. | Same caveat — pushing it onto the silicon path may produce IOKit failures. |
RenderBox.framework, WebGPU.framework, Espresso.framework |
Already covered above (graphics/ML). | — |
CoreVideo.framework |
Video pipeline / capture surfaces. Caches a global. | Similar story to the accel libs. |
StoreKit.framework (storekitd), AppStoreDaemon (appstored), AppStoreUtilities, Applications/StoreKitUISceneService |
All of the App Store / IAP daemons and their host UI. They tag receipts with VM status. | Best candidate to patch together, because making the Store stop labelling sessions as "vm=1" while everything else stays the same is exactly the kind of selective lying the user asked for. Receipts and SKUI flow start looking like a real device. |
ApplePushService (ApplePushService) |
APNS client. APNS connection establishment includes a "device characteristics" payload. | Patching makes the APNS handshake claim the device is bare metal. Useful if push tokens are silently downgraded for VMs. |
AAAFoundation, AuthKit, IDSFoundation, DeviceIdentity, DeviceCheckInternal |
Apple ID / iCloud / iMessage attestation and DeviceCheck (anti-fraud) plumbing. Every one of them caches is_vmm to feed into device-binding/anti-abuse decisions. |
Patching the AuthKit/IDS/DeviceCheck callers individually is the conservative way to make “Sign in with Apple ID”, iMessage activation, and DeviceCheck attestations stop self-flagging as VM, while leaving compute paths (CoreML, ANE) alone. High-leverage for "make iMessage/iCloud trust this device" without disabling kernel-level VM behaviour. |
MobileActivation.framework, MobileActivationMigrator |
Activation flow + migration tool. | Activation today uses kern.hv_vmm_present to decide whether to take the fastlane / dev path. Patching would normalise activation. |
CorePrescription.framework (+ XPC service) |
Health-data prescription store. | The check is almost certainly "don't sync prescriptions on a VM"; patching here gates Health/Rx data sync. |
EmailFoundation.framework |
Mail. | Likely a mail-account heuristic; patching is benign. |
PhotoFoundation.framework |
Photos. | Likely a "hide some assets on VMs" check. |
FindMyBase.framework |
Find My. | Anti-spoof gate. |
TrialServer.framework |
Internal A/B / trial-rollout client. | Trials' "exclude VMs" gate. |
WatchdogServiceManagement |
The watchdog manager. | Note string@0x272725b23 lives in the same page as its own __text; this is a tiny binary with one and only one consumer. Patching is local and self-contained. |
VisionKitCore.framework, CoreCDP.framework |
Vision/CDP. Single sysctl, cached. | — |
AirPlaySupport.framework |
AirPlay. Cached flag. | Likely a "no AirPlay receiver on VM" gate. |
DVTInstrumentsUtilities.framework |
Xcode Instruments support library. | Dev-only; cosmetic. |
Applications/CheckerBoard.app |
Apple's internal CoreAccessibility test app — the build-time sample most likely link-imports the same telemetry helper as everyone else. | Cosmetic. |
HostInferenceProviderService.appex |
On-device generative-AI inference host. | Same family as the ML accel libs above; gate to compute paths. |
Why the kernel-side patch breaks display while a user-side patch does not
The kernel-side patch (returning 0 from sysctl_vmm_present /
IODTGetDefault("vmm-present")) flips the answer for every consumer
in the boot pipeline, including kernel-internal users of vmm-present
(IOKit, IOSurface, display drivers, AGX). The display path consumes the
flag for its own paravirt routing — that's the symptom you observed.
A user-mode patch only changes what landed in the 35 dylib/exe call sites above. None of them are on the display bring-up path; they all run post-launchd, after the framebuffer/IOSurface plumbing is already up. So the display works as VM, while the higher-level "this is a VM" hints are suppressed wherever you choose.
Recommended patch surface
Given the vphone-cli design intent ("VM-aware where it has to be, device-like where it can be"), the smallest set that buys the most is:
libMobileGestalt.dylib— single biggest fan-in. Many other processes ask MG instead of callingsysctlbynamedirectly, so this one patch propagates.CorePrescription,WatchdogServiceManagement,EmailFoundation,PhotoFoundation,FindMyBase,AirPlaySupport,TrialServer,CoreCDP,VisionKitCore— the consumer apps/services. Patching them gives the device-like surface for sync, mail, photos, find-my, etc.AAAFoundation,AuthKit,IDSFoundation,DeviceIdentity,DeviceCheckInternal,MobileActivation,MobileActivationMigrator,ApplePushService— identity / push / activation. Patching here normalises the device's posture against Apple's anti-abuse signals.StoreKit/storekitd,AppStoreDaemon/appstored,AppStoreUtilities,StoreKitUISceneService— App Store + IAP. Patch as a group.- DO NOT patch (most likely):
CoreML,CoreRE,Espresso,AppleNeuralEngine,RenderBox,WebGPU,caulk,CoreVideo,IOSurfaceAccelerator,HostInferenceProviderService. These are the compute/accel libraries — pushing them onto the silicon path on a VM is the most likely way to cause new failures (no real ANE / no real AGX / no real H.265 hardware encoder available to take the call). - Ignore the false positives in section B entirely — they don't read the sysctl.
The exact byte-level patch shape that works for every CANONICAL site is
identical to what you already have in the kernel: turn the 1-bit boolean
that gets cached after the call into a constant 0. The ARM64 idiom
cset wN, ne (after cmp wM, #0) → mov wN, #0 (3 byte-pattern
substitutions: 0x9f1f00b1 zero immediate move, etc.) is a one-instruction
patch per consumer.
If you'd rather skip the call entirely, replace the bl <sysctlbyname>
with mov w0, #0 (success) and zero the result on the stack — also
one instruction, but now w0=0 is read as the int result so the
post-call boolean naturally becomes 0.
Files
outputs/hv_vmm_present_xref.json— full per-binary xref dump (string addresses, xref addresses, classification, surrounding disassembly).research/hv_vmm_present_xref.json— same dump committed in-tree.
Patcher implementation (Dev + JB only)
Implemented as part of the existing cfw_install_dev.sh /
cfw_install_jb.sh flows; the regular cfw_install.sh is left
untouched (per design — the regular variant stays "as honest as
possible", and the device-likeness layer is opt-in via the dev or
jb variants).
Patch shape, every site — 1-byte cstring mangle:
cstring "kern.hv_vmm_present\0" → "Xern.hv_vmm_present\0"
^ byte 0 = 'k' (0x6B) ^ byte 0 = 'X' (0x58)
The kernel returns ENOENT for the mangled name; the canonical
post-call error check (cbnz w0, skip / cmp w0,#0 ; b.ne skip)
takes the skip-cache path, so the cached "is_vmm" byte stays at its
initial 0 (stack pre-zeroed by stur wzr or BSS-zero for a global
oldp). Idempotent: a re-run can't find the unmangled cstring and
no-ops.
We don't touch executable code — only one byte of string data in
__TEXT,__cstring. The kernel call still happens (so sysctl
instrumentation still sees activity), it just fails the
name-to-MIB translation.
What gets patched
| Tier | Scope | Where applied |
|---|---|---|
| DSC dylibs (20) | Identity / store / consumer-services (see list above) | DSC chunks inside the SystemOS Cryptex DMG, while it is mounted on the host |
| Standalone (6) | MobileActivationMigrator, CheckerBoard, StoreKitUISceneService, storekitd, appstored, CorePrescriptionService.xpc | Pulled from the device rootfs over SSH, patched, ldid-signed, pushed back |
| Compute / accel (10) | CoreML, Espresso, ANE, CoreRE, RenderBox, WebGPU, caulk, IOSurfaceAccelerator, HostInferenceProviderService | Not patched — leaving these alone preserves their VM-aware fast-path bypasses |
Source files
scripts/patchers/cfw_patch_hv_vmm.py— standalone cstring patcher (patch_hv_vmm(filepath)): finds occurrences of"kern.hv_vmm_present\0"in__cstring(and the objc method/class name pools, for safety) and rewrites byte 0 from'k'to'X'.scripts/patchers/cfw_dsc_chunks.py— chunked-DSC byte-level helper: vmaddr ↔ chunk-fileoff mapping, executable-mapping cstring scan, byte read/write at a vmaddr, and Mach-O header walk-back to resolve a vmaddr to the containing dylib's install name.scripts/patchers/cfw_patch_hv_vmm_dsc.py— DSC-native orchestrator. No externalipswdependency. For every"kern.hv_vmm_present\0"occurrence found in any executable mapping, walks back to the containing dylib's Mach-O header, readsLC_ID_DYLIB, and — if the install name is in the explicitPATCH_INSTALL_NAMESwhitelist — rewrites the first byte of the cstring throughDSCChunks.write_at_vma. After all whitelisted byte-mangles, callscfw_dsc_codesign.reattest_modified_pagesto recompute the SHA-256 slot hash in the containing DSC chunk'sCS_CodeDirectoryfor every affected 16 KiB page (see below — this is required on hardware withcodeSigningMonitor == 2). The whitelist is one entry per line in the source file so an operator can comment out individual dylibs during bisection. (An earlier draft usedbl _sysctlbyname→movn w0,#0instruction patching and anipsw dyld extractpath; both were scrapped — the former because it modifies executable bytes which has the same per-page-hash risk as the cstring approach but a larger surface area, and the latter becauseipswreturns a Mach-O whose load-command file offsets reference the original DSC chunk's file layout, making it unparseable as a standalone Mach-O.)scripts/patchers/cfw_dsc_codesign.py— page-hash re-attestation. Without this pass, every byte-mangle invalidates the affected 16 KiB page's SHA-256 slot hash in the chunk-levelCS_CodeDirectory. On iPhone17,3 / iOS 26.1 (codeSigningMonitor == 2), TXM enforces per-page integrity at demand-page-in (XNU defers exec-page validation to TXM forcsm_associatedmappings —research/reference/xnu/osfmk/vm/vm_fault.c:2763-2780). Concrete repro before re-attestation:lsd-2026-05-11-035240.ipsfaults inside MobileActivation's __TEXT at offset0x13DC0, in the same 16 KiB page as the mangled cstring (offset0x106C6). The re-attestation rewrites only the 32-byte slot whose page contains a modified byte; the CDHash side-effect (CD blob hash changes when slots change) is accepted by TXM at DSC mount time (empirically verified — device boots with re-attested DSC).scripts/patchers/cfw.py— addspatch-hv-vmm <binary>andpatch-hv-vmm-dsc <chunks_dir>subcommands.scripts/patch_hv_vmm_userland.sh— thin wrapper used by the install scripts.scripts/cfw_install_dev.sh— DSC patch is applied while the SystemOS Cryptex DMG is still mounted on the host (inside step[1/7]); standalone binaries are patched over SSH in new step[6.5/7].scripts/cfw_install_jb.sh— pre-step before invoking the regularcfw_install.sh: decrypts the SysOS Cryptex into the same cache filecfw_install.shwould, mounts it, applies the DSC patch, unmounts. The unmodifiedcfw_install.shthen sees the cached (already-patched) DMG. Standalone binaries are patched in new step[EXP-JB-3.5]after the base install completes (EXP variant only).
Dylibs that must keep seeing VM=1 (graphics + accel passthrough)
Empirically validated: lying to these dylibs about VM presence breaks
the boot graphics path or the compute/accel fast paths. They MUST
read 1 (or the original truthful value) at runtime to function. Under
the new blacklist-flip design they are therefore NOT in
DONT_PATCH_INSTALL_NAMES — which means the byte-5 mangle IS applied
to their cstrings, they query the renamed kernel sysctl
kern.Xv_vmm_present, and the kernel returns 1 to them just like a
stock device would.
| Dylib | Why a lie breaks it |
|---|---|
/usr/lib/libMobileGestalt.dylib |
Highest fan-in answer source. Forcing it to think "not a VM" sends the whole display-init chain into paths that demand real silicon display hardware. |
PrivateFrameworks/PhotoFoundation.framework/PhotoFoundation |
Photos / image pipeline ties into the boot display path (asset decode + thumbnail rendering touches the same Metal surfaces the boot UI uses). |
PrivateFrameworks/AirPlaySupport.framework/AirPlaySupport |
AirPlay screen-mirroring plumbing registers as a display source at boot; lying about VM mode flips the registration path. |
PrivateFrameworks/VisionKitCore.framework/VisionKitCore |
Camera / vision-intelligence stack hooks into the boot-time display chain on this device. |
Frameworks/CoreVideo.framework/CoreVideo |
Directly in the display pipeline — lying routes the boot display path through code that assumes physical capture surfaces. |
Frameworks/CoreML.framework/CoreML |
Compute fast-path. The VM-aware bypass is what prevents CoreML from trying to drive real ANE. |
PrivateFrameworks/Espresso.framework/Espresso |
Same — neural-net inference dispatch. |
PrivateFrameworks/AppleNeuralEngine.framework/AppleNeuralEngine |
ANE driver bring-up. |
PrivateFrameworks/CoreRE.framework/CoreRE |
Reality Engine dispatch. |
PrivateFrameworks/RenderBox.framework/RenderBox |
Graphics dispatch. |
PrivateFrameworks/WebGPU.framework/WebGPU |
GPU dispatch. |
PrivateFrameworks/caulk.framework/caulk |
HW codec dispatch. |
PrivateFrameworks/IOSurfaceAccelerator.framework/IOSurfaceAccelerator |
IOSurface accelerator dispatch. |
ExtensionKit/Extensions/HostInferenceProviderService.appex/HostInferenceProviderService |
On-device ML inference host. |
Under the new design these libs are not enumerated in code — they
implicitly fall into "not blacklisted → patched → sees VM=1" by
virtue of NOT being in DONT_PATCH_INSTALL_NAMES. To explicitly
exclude a different lib from the patch (force it to lie about VM
presence), add its install name to the blacklist. The build / install
flow is idempotent in re-attestation (cfw_patch_hv_vmm_dsc.py will
detect already-mangled cstrings on a re-run and re-sync slot hashes
for their pages), so toggling an entry in or out of the blacklist
doesn't require a fresh IPSW unpack — but doing so after a previous
install requires rm -f vm/.cfw_temp/CryptexSystemOS.dmg to force a
fresh DSC mount.
What is verified vs. inferred
Verified by direct disassembly + Mach-O byte inspection:
- The 35 CANONICAL sites use
sysctlbyname("kern.hv_vmm_present", &int, &len, NULL, 0). - The 4 entitlement-plist-only matches are pure plist substring hits.
- The 7 sandbox-profile-only matches are inside a compiled SBPL blob
(string lives in
__TEXT,__const, no executable xref, identical byte signature across the seven binaries). - The 9 DEAD-string matches really are unreferenced (no ADRP/LDR computes the address; no 8-byte little-endian or 36-bit chained-fixup shape contains it).
SoundAnalysisuses our string only as an offset anchor.
Inferred from framework / binary path and the canonical caching shape (not from runtime tracing): the mapping in the table above of "what each caller uses the result for". The disassembly proves that they read the OID and how; it does not prove what they branch on. Every "likely role" / "patching effect" claim above should be confirmed by running the patched dylib and observing behaviour.