The patch-16 entitlement-gate matcher keyed on source line IDs
(`mov w8, #0x332D / #0x333B`); on 26.4 both the value and the register drifted,
so it found nothing and silently skipped (worked on 26.1/26.3).
Re-anchor the deny blocks on their stable panic strings ("This operation needs
entitlement" and "lookup takes place out of a volume group, but the source
volume is in one"), scoped to the routine by also requiring the
`handle_get_dev_by_role` function-name string — this excludes the adjacent
`handle_volume_class_keybag_op`, which shares the entitlement message. Yields the
same 3 gates on 26.1/26.3 (parity) and now 3 on 26.4.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Two pre-existing iBoot patches silently skipped on some cloudOS versions
(verified by cross-version disassembly of the vresearch101 iBEC/LLB payloads):
- bootx precondition (iBEC): the `BL <bit-getter>; TBZ w0,#0 -> panic` construct
is a 26.4+ iBoot addition and is genuinely absent on 26.1/26.3 (which boot fine
without it). Treat "construct not present" as an informational skip, not a
failure; ambiguity (>1 gate) still hard-fails.
- LLB rootfs size gate: the bare `cmp x8, #0x400` is not unique — 26.4 LLB has
three (only one is the size gate, the other two are followed by `b.hi`). Anchor
on the `cmp x8,#0x400 ; b.hs` pair, unique on 26.1/26.3/26.4.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The 26.5 implementation embedded multiple build-specific values:
- a hardcoded byte offset 1056 to find `_sSourceList`
- a hardcoded stack-frame offset (#576) in the per-source filter scan
- hardcoded ivar offsets 0x08/0x18/0x10/0x18/0x48 on
BWFigCaptureDevice / BWFigCaptureStream
- hardcoded image VMAs 0x1ae6ff05c, 0x1ae2bd414, 0x1ae2c353c for
three error-suppression byte-patches
- two `#if 0` blocks pinning more 0x1ae* VMAs
Refactor every site to a runtime-resolved equivalent.
1. _sSourceList: structural ARM64 anchor chain rooted at the exported
FigCaptureSourceServerStart symbol — every link is a stable pattern
that survives DSC byte-offset shifts, stub-call layout changes, and
LC_SYMTAB local-symbol stripping:
FigCaptureSourceServerStart (exported, retained on every build)
walk for `cmn x?, #0x1 ; b.ne <wrapper>` (onceToken check)
wrapper (single-insn `bl <cold.1>` site)
cold.1 (static helper; 5-6 instructions)
`adrp x1, ... ; add x1, x1, #imm` (block-constant addr)
block constant (struct __Block_literal in __DATA_CONST)
+0x10 = invoke pointer (PAC-stripped) = dispatch_once body
init block-invoke
walk for `bl <X> ; adrp + str x0, [Xn, #imm]` pairs
(each "store fn result
into a static global")
pick the first slot whose stored value is a heap CFArray
(filters the lock-store
at #0 — that's a void*
mutex handle, not an
array)
LC_SYMTAB is still consulted first as a fast deterministic path for
builds that happen to retain `_sSourceList` as a regular nlist entry;
the structural chain is what actually fires on stock 26.1/26.3.1/26.5
DSCs (which strip static data symbols).
2. Per-source filter LDR x2 anchor: mask the imm12, accepting any
sp-relative 64-bit load into x2 regardless of the compiler-chosen
stack-frame slot.
3. BWFigCaptureDevice / BWFigCaptureStream ivar offsets: resolved at
synth-class init via class_getInstanceVariable + ivar_getOffset on
the parent class. Required ivars (deviceID, portType, uniqueID)
abort class registration on miss; the streaming BOOL is optional
(skip the YES poke instead of aborting). New vcc_resolve_ivar
helper walks a NULL-terminated candidate-name list to tolerate
underscore-prefix convention differences.
4. -[FigCaptureCameraSourcePipeline requiresMasterClock] prologue:
resolved via LC_SYMTAB by name (two underscore-prefix variants),
PAC-stripped, gated on a `pacibsp` insn1 sanity anchor before
rewriting to `mov w0, #0 ; ret`. The function isn't in the ObjC
method table on observed builds (so class_replaceMethod won't
intercept) — the byte-patch is the only working path.
5. _cs_addObjectToStreamsAttributes and -[BWFigVideoCaptureStream
initWithCaptureStream:…] -12783 bail sites: both prepare the
OSStatus via MOVN encodings (0x12863dd4 for w20, 0x12863dc8 for
w8). The new vcc_scan_and_patch helper finds every occurrence of
each encoding in __text and rewrites it to MOVZ #0. -12783 is a
capture-specific OSStatus and the daemon's only consumer in the
VM is the synth source, so over-application is benign.
Validator fixes (kept from the original 26.1/26.5 work):
- arm64e ISA class-pointer mask: 0x00007FFFFFFFFFF8 (44-bit class
field, bits 3-46) per libobjc's ISA_MASK. The previous mask
captured bit 47 (magic-signature region), so two pointers to the
same class produced different masked values when bit 47 differed.
- Pointer dereferences during slot validation gated by
`malloc_zone_from_ptr` so a stale/bogus heap pointer in a
candidate slot can't trap the daemon during init. (vm_read /
vm_read_overwrite were considered but cameracaptured's sandbox
returns KERN_DENIED on intra-task vm_read on iOS 26.x.)
Helpers in scripts/vcamcaptured/libvcamcaptured.m:
vcc_safe_read_ptr pointer-read wrapper
vcc_slot_value_is_cfarray malloc_zone + ISA-class check
vcc_collect_call_then_store_globals walk a function body for
"BL <X>; adrp + str x0,
[Xn, #imm]" pairs
vcc_resolve_ivar class_getInstanceVariable
wrapper with candidate-name list
vcc_scan_and_patch __text scan + per-occurrence
vcc_patch_word wrapper
VCC_ISA_CLASS_MASK arm64e 44-bit class-pointer mask
The two dead `#if 0` byte-patch blocks (referencing 0x1ae2b5284 and
0x1ae2b4c90, with the captureSession_buildGraphWithConfiguration
thumbnail / preview-sink bail-bypass commentary) are removed along
with their explanatory comments. scripts/cfw_install_exp.sh's
comment that mistakenly described a non-existent "Patch #6" inside
_captureSourceServer_handleCopySourcesMessage is rewritten to
describe the actual DSC patches (NU short-circuit + AVF authorization,
both already version-agnostic via `ipsw dyld symaddr`).
Validated end-to-end on:
iOS 26.1 build 23B85
iOS 26.3.1
iOS 26.5 build 23F77
All three return the same `vphone:vcam:0` synthetic camera as the
default video device and deliver real JPEG frames through the modern
AVCapturePhoto delegate path in continuitycaptured / Camera.app.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Replace the two giant hardcoded selectors (the 27-arg AVCapturePhoto
init and the 32-arg +resolvedSettingsWithUniqueID:… factory) with a
prefix-lookup + label-driven NSInvocation builder. This survives
Apple adding or removing args between iOS releases without code
changes: the discovered selector decides arg count and order, the
resolver block fills the args we care about by label
("timestamp", "photoSurface", "uniqueID", "photoDimensions", etc.),
and unknown labels get nil/zero from the runtime type encoding.
cfx_find_selector_by_prefix(cls, prefix, classMethod)
Walks class_copyMethodList on cls (or its metaclass for class
methods), returns the matching selector with the most colons.
Highest-arg-count match wins so a future Apple revision that
adds a new arg in the middle is still found.
cfx_normalize_first_label(NSString *)
Strips "initWith" / "resolvedSettingsWith" and lowercases the
first char of the remainder so the leading component matches
the same label convention as the rest of the selector.
cfx_invoke_with_labeled_args(target, selector, resolver)
Builds the NSInvocation, iterates selector components, calls
the resolver block once per arg with (label, typeEnc, outBuf).
Block writes the value via the appropriate cast (CMTime,
IOSurfaceRef, __unsafe_unretained id, NSInteger, etc.) or
leaves outBuf zeroed.
Builders refactored:
- cfx_build_resolved_settings now fills only uniqueID +
photoDimensions + previewDimensions; everything else stays
zero/nil (Apple's impl tolerates that on builds where the
factory itself works at all).
- cfx_build_avcapturephoto_with_request fills timestamp,
photoSurface, photoSurfaceSize, processedFileType, metadata,
captureRequest, sequenceCount, photoCount, sourceDeviceType.
Every other surface/dictionary arg defaults to nil.
End state: net +169/-96 lines, zero hardcoded full selectors,
photo synthesis remains functionally identical on 26.5 and is
prepared for arg-list drift on future iOS revisions.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Universal injection mechanism: any process where AVFoundation is
loaded (Camera.app, continuitycaptured, third-party apps, system
daemons — anything dyld pulls AVF into) automatically gets libcamfix
via TweakLoader. No per-bundle plist filter, no allowlist entries.
scripts/tweakloader/TweakLoader.m
New Filter.Frameworks key. A tweak's plist may list framework
names; TweakLoader matches the path containing
"/<name>.framework/". Already-loaded frameworks trigger an immediate
dlopen; not-yet-loaded frameworks register a
_dyld_register_func_for_add_image callback that fires when the
named framework appears.
Two-tier engagement:
- Framework-filtered tweaks scan + schedule in EVERY process,
self-limiting at runtime. Cost in non-AVF processes is one
dir scan + a few plist parses + one callback registration.
- Non-framework tweaks (Bundles/Executables or no filter) keep
the existing .app/+kVPhoneAllowedDaemonPaths gate so we don't
drop arbitrary tweaks into launch-critical daemons.
CRITICAL safety: dyld invokes add-image callbacks SYNCHRONOUSLY
inside its loader lock. dlopen from within that callback recurses
and can deadlock or crash early daemons. The actual dlopen is
handed off to a background queue (dispatch_async) so it runs after
dyld is idle.
Defensive: each per-tweak block is @try/@catch wrapped so a
malformed plist or Foundation quirk in an early-boot daemon can't
crash the process and trigger a launchd respawn loop.
scripts/camfix/libcamfix.m
Constructor no longer eagerly installs hooks. Instead registers a
_dyld_register_func_for_add_image callback and installs hooks the
first time AVFCapture's mach header is observed (idempotent via
dispatch_once). Whether libcamfix loads before or after AVFCapture,
hooks land exactly once.
cfx_capturePhoto_hook now drives the MODERN
-[<AVCapturePhotoCaptureDelegate> captureOutput:
didFinishProcessingPhoto:error:] path in addition to the deprecated
CMSampleBuffer one. The synthesized AVCapturePhoto uses nil
captureRequest (there's no CAMCaptureEngine outside Camera.app —
msgSend to nil during init returns 0 safely). Photos tagged with
associated JPEG/CGImage so fileDataRepresentation /
CGImageRepresentation return our bytes regardless of which delegate
protocol the client implements.
scripts/camfix/libcamfix.plist
Filter.Frameworks = ["AVFoundation"]. Replaces the previous
Bundles=["com.apple.camera"] filter.
scripts/cfw_install_exp.sh
build_libcamfix install_name reverted to /var/jb/Library/
MobileSubstrate/DynamicLibraries/libcamfix.dylib (TweakLoader
location). [JB-4.2] deploys dylib + plist together.
Verified on fresh `make setup_machine` install of 26.5:
- 373+ distinct AVF-using processes auto-load libcamfix at boot,
including watchdogd / amfid / backboardd / SpringBoard /
cameracaptured / continuitycaptured.
- Camera.app: preview live, photos save, shutter works past
many consecutive captures.
- continuitycaptured: a vanilla AVCapturePhotoCaptureDelegate
using the documented capturePhotoWithSettings:delegate: API gets
a real 1280x720 JFIF JPEG via the modern delegate path.
- Full reboot cycle stable.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
libcamfix.dylib is loaded into Camera.app (com.apple.camera) by
TweakLoader (the .app/-rule covers Camera.app automatically; the plist
filters to Bundles=[com.apple.camera]). It bridges the vphone shm
frames from libvcamcaptured into Camera.app's normal photo + preview
pipeline so the user can take real photos via the standard shutter.
Hooks (only fire for connections backed by AVCaptureDevice uid
"vphone:vcam:0"):
- _setActiveFormat: substitute device.formats.firstObject when
the session-preset->format lookup hands us a
nil format (would otherwise throw at launch).
- capturePhoto deliver a CMSampleBuffer (built from shm) via
the deprecated didFinishProcessingPhotoSample
delegate path — kept for test harnesses that
use the documented AVCapturePhotoOutput API.
- beginMomentCapture / commitMomentCaptureToPhotoWithUniqueID:
Camera.app's actual shutter path. Skip orig
(would throw), stash the delegate at begin,
drive a synthesized AVCapturePhoto at commit.
- cancelMomentCaptureWithUniqueID: no-op (orig would throw).
- AVCaptureSession _setRunning: / _setInterrupted: setters swallowed
for vcam-bound sessions, and isRunning /
isInterrupted getters force YES / NO so
Camera.app's "preview live" poll keeps the
viewfinder visible past ~4-5 s.
- AVCaptureVideoPreviewLayer: scan UIApplication.windows at 1 Hz
for layers bound to a vcam session and pump
CGImage frames into layer.contents at 30 Hz.
- AVCapturePhoto fileDataRepresentation / CGImageRepresentation:
when the photo we synthesized is the receiver,
return the JPEG / CGImage we built from shm
instead of asking the (non-existent) photo
surface to encode itself.
- CAMStillImageCaptureRequest: dynamically add three stubs
(resolvedSettings, unresolvedSettings,
lensStabilizationSupported) so AVCapturePhoto's
private 27-arg init does not throw on the
CAM-internal request we pass in.
Synthesized AVCapturePhoto construction:
- extract the real CAMStillImageCaptureRequest for the current uid
from CAMCaptureEngine._resultsQueueRegisteredStillImageRequests
(Camera.app's pending-photo dict),
- hand-build a minimal AVCaptureResolvedPhotoSettings via
class_createInstance + ivar writes for uniqueID + dimensions +
empty NSArray ivars (CFRetained so the dealloc chain stays valid),
- feed both into AVCapturePhoto's documented 27-arg
initWithTimestamp:photoSurface:... via NSInvocation,
- tag the photo with the JPEG bytes via objc_setAssociatedObject
so the fileDataRepresentation hook returns them.
Full AVF + CAM internal delegate sequence fired at commit time:
willBeginCaptureBeforeResolvingSettingsForUniqueID,
willBeginCaptureForResolvedSettings, willCapturePhotoForResolvedSettings,
didCapturePhotoForResolvedSettings, didFinishProcessingPhoto:error:,
didFinishCaptureForResolvedSettings:error:,
_didFinishStillImageCaptureForUniqueID:error:, and crucially
captureOutput:readyForResponsiveRequestAfterResolvedSettings:.
Without that last "responsive ready" signal AVF's 2-deep pipeline
never frees its slots and Camera.app's shutter stops accepting
input after the 2nd capture.
Install wiring in scripts/cfw_install_exp.sh:
- build_libcamfix() — clang -arch arm64e -fobjc-arc -Os, frameworks
AVFoundation / CoreImage / CoreMedia / CoreVideo / Foundation /
ImageIO / IOSurface / MobileCoreServices / Photos / QuartzCore /
UIKit, ldid-signed.
- [JB-4.2] scp the dylib + plist into procursus/Library/MobileSubstrate/
DynamicLibraries (same location as libvcamcaptured) and chmod /
chown so TweakLoader picks them up on next boot.
End state: Camera.app on EXP shows live preview from the host-supplied
vcam frames, the shutter takes real photos that get saved into Photos
via Camera.app's own pipeline (no PHPhotoLibrary back-channel), and
the shutter button keeps working across many consecutive captures.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
libvcamcaptured.dylib is loaded into /usr/libexec/cameracaptured via
TweakLoader and registers a synthetic FigCaptureSource backed by the
vphone shm region. From AVF's point of view there is now a normal
"vphone:vcam:0" camera device that streams BGRA frames at the
session's requested width/height.
scripts/vcamcaptured/ .gitignore (drop built .dylib),
Makefile, libvcamcaptured.m,
libvcamcaptured.plist (filter:
Executables=["cameracaptured"]).
scripts/tweakloader/TweakLoader.m add /usr/libexec/cameracaptured
to kVPhoneAllowedDaemonPaths so
TweakLoader engages in a daemon
(not just .app/) processes.
scripts/cfw_install_exp.sh build_libvcamcaptured() helper
(clang + CoreMedia/CoreVideo/
Foundation, ldid-signed) and
new [JB-4.1] section that
scp's the dylib + plist into
procursus/Library/MobileSubstrate/
DynamicLibraries.
Pairs with the host vphone-cli camera server + vphoned vcam vsock
listener already in this branch.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Guest-side counterpart to VPhoneCameraServer. vphoned runs inside
the VM and now hosts an extra vsock listener on port 1338 that
receives BGRA frames from the host and writes them into a memory-
mapped shm region at
/var/jb/var/mobile/Library/vphone-vcam-frame.shm with a packed
header (seq monotonic, width/height, bytes_per_row, pixel_format,
timestamp_ns, frame_index, pixels_length).
vphoned_vcam.h packed shm header layout + filename const.
vphoned_vcam.m VPVcamServer: accept loop, header parse,
seq-bump write, file-based debug log for
first-boot post-mortem.
vphoned.m Boot the vcam server alongside the existing
port-1337 control daemon.
Pairs with the host VPhoneCameraServer (1338 client) and the guest
libvcamcaptured (shm reader) added in the following commits.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Mac-side feeder for the virtual camera. The host generates frames
(test pattern or video file) and pushes BGRA buffers over vsock to
the guest daemon vphoned, which mirrors them into the shm region
that the in-VM libvcamcaptured + libcamfix consume.
VPhoneCameraServer @MainActor vsock client to vphoned port
1338. Reconnects on transport failure
so the host can survive guest reboots.
VPhoneFrameProducer Test-pattern (hue-rolling gradient) and
VideoFile producers; emit BGRA at the
session's requested width/height/fps.
VPhoneMenuCamera Camera menu: pick source, start/stop
the feeder, show stream status.
VPhoneMenuController/Connect/AppDelegate
Wire the menu into the existing menu
bar and start the server alongside the
other vsock services at VM boot.
End state: with a vphoned listener on the guest side, host-generated
BGRA frames appear in the shm region the camera daemon reads from.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Three pieces of plumbing that together let Camera.app reach the
viewfinder UI on the EXP build:
1. DeviceTree: synthesize a /product/camera node so AVCaptureDevice
discovery + permission gating finds a "front" camera slot for
subsequent vcam injection (FirmwarePatcher/DeviceTreePatcher.swift).
2. DSC patch family in scripts/patchers/cfw_patch_camera_dsc.py:
- NeutrinoCore short-circuit: rewrite the five
+[_NUStyleTransfer*Processor processWithInputs:arguments:output:
error:] class methods to `mov w0, #0; ret`. Without this Camera.app
crashes inside NeutrinoCore the moment it tries to render the
style picker.
- AVCaptureDevice auth always-authorized:
+[AVCaptureDevice authorizationStatusForMediaType:] -> `mov w0, #3;
ret` (AVAuthorizationStatusAuthorized=3). Any process that probes
camera authorization gets "Authorized" without going through TCC.
3. scripts/patch_camera_userland.sh + cfw.py registration so the install
pipeline applies the two patches above against the chunked DSC during
`make cfw_install_exp`.
Camera.app now launches and shows preview UI on EXP, even though the
actual vcam pipeline is wired up by the libvcamcaptured / libcamfix
commits that follow.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Adds the final EXP-only step: rewrite the userland-visible
`ProductBuildVersion` in `SystemVersion.plist` to a chosen build
identifier. Gated on the `SPOOF_BUILD` env var — when unset/empty, the
step is skipped entirely and the build identifier stays at whatever the
IPSW shipped.
The iPhone IPSW we install from ships with build identifier `23B85`
(iOS 26.1). iOS displays this string in Settings -> General -> About ->
"Build" and exposes it through `MGCopyAnswer("BuildVersion")`,
CoreFoundation's `_CFCopyServerVersionDictionary`, App Store telemetry,
and every other framework path that reads
`/System/Library/CoreServices/SystemVersion.plist`.
The build identifier lives in exactly two on-device plist files. Both
are plain XML/binary plists (no Apple-side per-file signature), and
both live on volumes that are writable at install time:
/System/Library/CoreServices/SystemVersion.plist (rootfs)
/private/preboot/Cryptexes/OS/System/Library/CoreServices/SystemVersion.plist (preboot)
EXP-JB-7 rewrites the `ProductBuildVersion` key in both plists to the
target value (typical: `23F77`). `ProductVersion` (`26.1`),
`ProductName` (`iPhone OS`), `BuildID`, `SystemImageID`, and
`ProductCopyright` are left untouched.
Order of operations: EXP-JB-7 runs AFTER EXP-JB-6 (post-restore DT
rewrite) so the post-restore identity work on `/mnt5` has completed
before the Cryptex's SystemVersion.plist (same volume) is touched.
Does NOT flip:
- `sysctl kern.osversion` — comes from a kernel global initialized
from boot args at boot time, not from this plist. To change it
would require rebuilding the kernelcache with a different
`OS_BUILD_VERSION` or patching the boot-args path — out of scope.
- `SystemVersionCompat.plist` — carries a legacy iOS-19 marker for
MacCatalyst-style queries; not user-visible, deliberately untouched.
- `scripts/patchers/cfw_patch_build_version.py` — host-side
plistlib-based rewriter. Auto-detects XML vs binary plist format
and preserves it on write. Idempotent — a re-run on an
already-patched plist exits without rewriting.
- `scripts/cfw_install_exp.sh` — EXP-JB-7 phase: gated on
`SPOOF_BUILD`; for each of the two plist paths it scp_from's the
file to host, runs the patcher with the target id, scp_to's the
file back. Tolerates missing-on-device with warn+continue.
Invocation:
make setup_machine EXP=1 SPOOF_BUILD=23F77
make cfw_install_exp SPOOF_BUILD=23F77
JB and DEV install scripts do NOT carry this step.
The three restore-fatal DT root properties (root `model`, root
`target-type`, root `compatible[0]`) cannot be edited at fw_patch
time because `restored_external` / iBoot's restore mode cross-checks
them against the BuildManifest's signed `SupportedProductTypes`.
Editing them in the IPSW's `devicetree.im4p` causes the restore to
fail partway through.
But the cross-check fires ONLY during installation. After restore
completes, subsequent boots validate IM4P contents against the IM4M
only, which the existing iBSS/iBEC/LLB `image4_validate_property_callback`
bypass patches accept regardless. So an additional DT edit applied
AFTER restore but BEFORE the device reboots into the rootfs is
in-policy with the project's existing trust-chain bypass.
EXP-JB-6 exploits this. While the install pipeline still has `/mnt5`
(the preboot volume) mounted on the ramdisk, `cfw_install_exp.sh`:
1. scp_from's `/mnt5/<boot-hash>/usr/standalone/firmware/devicetree.img4`
to the host.
2. Runs `scripts/patchers/cfw_patch_post_restore_dt.py`, which:
- Unwraps IMG4 -> IM4P -> LZFSE-decompresses -> DT flat-binary
blob (via pyimg4).
- Rewrites three root properties:
root `model`: iPhone99,11 -> iPhone17,3
root `target-type`: VPHONE600 -> D47
root `compatible`: reorder so D47AP is first, VPHONE600AP
second (IOKit's AppleVMApple1IO platform
bind still resolves via the second
entry; userland reads only the first
for `hw.model`).
- Re-compresses LZFSE -> repacks IM4P -> repacks IMG4 with the
ORIGINAL IM4M (the per-board ticket survives — the image4
bypass already accepts any payload contents).
3. scp_to's the modified img4 back to the same path.
4. The device reboots out of ramdisk; iBoot loads the modified DT;
kernel populates `machine_info` from the new property values.
Idempotent: the patcher detects target-state-already-met and exits
without rewriting.
Userland effects on next boot:
- `sysctl hw.machine` -> "iPhone17,3" (was "iPhone99,11")
- `sysctl hw.product` -> "iPhone17,3" (was "iPhone99,11")
- `sysctl hw.model` -> "D47AP" (was "VPHONE600AP")
- Settings -> General -> About -> Model Identifier picks up the new
ProductType after the gestalt cache rebuilds.
- `scripts/patchers/cfw_patch_post_restore_dt.py` — host-side
img4 <-> IM4P <-> DT flat-binary round-trip via pyimg4. Mirrors the
DT format parser/serializer from `DeviceTreePatcher.swift`.
- `scripts/cfw_install_exp.sh` — EXP-JB-6 phase: discovers the
boot-manifest-hash via the same `get_boot_manifest_hash` helper used
by earlier install steps; tolerates a missing devicetree.img4 with
warn-and-skip. JB and DEV install scripts do NOT carry this step.
Splits `DeviceTreePatcher`'s property-patch list into two arrays:
- `basePropertyPatches` (4 entries: `serial-number`,
`home-button-type`, `artwork-device-subtype`,
`island-notch-location`) — applied for every variant. Behaviour
identical to pre-split.
- `identityPropertyPatches` (8 entries — Tier 1b + 1c userland-facing
identity surfaces) — applied only when `includeIdentityPatches` is
true, which `FirmwarePipeline` sets exactly when `variant == .exp`.
The 8 EXP-only identity properties flip userland-visible identity toward
D47AP / iPhone17,3:
- Tier 1b (5 properties, slot-length-preserving rewrites):
device-tree.target-sub-type: VPHONE600AP -> D47AP
device-tree.compatible[1]: iPhone99,11 -> iPhone17,3
(reorder, VPHONE600AP kept first
so IOKit's AppleVMApple1IO bind
still resolves)
device-tree/product.fdr-product-type: iPhone99,11 -> iPhone17,3
device-tree/product.sub-product-type: iPhone99,11 -> iPhone17,3
device-tree/product.unique-model: VPHONE600AP -> D47AP
- Tier 1c (3 properties — IOKit secondary matchers + Gestalt subtree
rename, matched against the real D47AP DT):
device-tree/arm-io.device_type: vresearch1-io -> t8140-io
device-tree/arm-io.soc-generation: VResearch1 -> H17
device-tree/product/vphone600-gestalt-variants.name (node rename):
vphone600-gestalt-variants
-> d47-gestalt-variants
Root `model` and root `target-type` are deliberately NOT in this list —
both have been empirically shown to break restore (signed-identity
cross-check in `restored_external`). Those edits run post-restore in a
later commit as EXP-JB-6.
- `sources/FirmwarePatcher/DeviceTree/DeviceTreePatcher.swift` — adds
`includeIdentityPatches: Bool = false` to init (backwards-compatible
default), stores it, splits `propertyPatches` into two static lists,
iterates base first and then optionally identity in `applyPatches`.
- `sources/FirmwarePatcher/Pipeline/FirmwarePipeline.swift` — passes
`includeIdentityPatches: variant == .exp` into the DT factory.
JB and other variants (regular, dev, less) leave the device's identity
properties untouched.
After the kernel-side OID rename (`KernelEXPPatchHvVmmRename`),
`sysctlbyname("kern.hv_vmm_present", ...)` returns ENOENT on this image.
`/usr/libexec/watchdogd` caches that answer at startup. On ENOENT the
cached byte stays at its BSS-zero default (`0`) and the downstream
`cbz w0, ...` at the IOWatchdog-lookup site takes a branch into
`_os_crash` -> `brk #1`; launchd's `_PanicOnCrash =
PanicOnConsecutiveCrash = true` flag in `com.apple.watchdogd.plist`
escalates the SIGTRAP to a kernel panic.
The cstring-mangle approach used for DSC dylibs doesn't apply here: we
want this binary to behave as if the sysctl returned 1, not as if it
returned ENOENT. Solution is a surgical 2-instruction patch that forces
the cached "am I a VM?" byte to 1 regardless of the sysctl result.
- `scripts/patchers/cfw_patch_watchdogd.py` — capstone-anchored pattern
matcher + Keystone-assembled 2-insn patch. Two functions match the
canonical caching shape on iPhone17,3 / iOS 26.1; both are patched.
Net effect: `cbnz w0, skip` -> NOP and `cset wN, ne` -> `mov wN, #1`,
forcing the cached byte to 1. watchdogd's pre-existing "detected
virtual machine environment, exiting..." clean-exit branch runs
instead of the trap path. Idempotent.
- `scripts/patchers/cfw_macho_codesign.py` — generic standalone-Mach-O
page-hash re-attestation. Parses `LC_CODE_SIGNATURE` directly, reads
page size from each `CS_CodeDirectory` header (4 KiB on watchdogd —
not the DSC's 16 KiB), handles short tail slot length
(`codeLimit - (n-1)*pageSize`), and updates every present CD. The
resulting cdHash change is accepted by the JB `patch_amfi_cdhash_in_trustcache`
kernel patch which accepts any cdHash; the patcher does NOT re-sign
with ldid (preserving the original Apple-issued code-signing
identifier is required for launchd's boot-task identity validation).
- `scripts/patchers/cfw.py` — adds `patch-watchdogd` subcommand.
- `scripts/patch_hv_vmm_userland.sh` — adds `watchdogd <binary>` op.
- `scripts/cfw_install_exp.sh` — invokes the patcher at step
`[EXP-JB-3.5]` on the live `/mnt1/usr/libexec/watchdogd` (scp-down,
patch, scp-up, chmod 0755). JB and DEV install scripts do NOT run
this step.
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.
Introduces a new `exp` firmware variant on top of JB and ports the
`hv_vmm_present` sysctl rename to it.
Variant infrastructure
----------------------
- Adds `case exp` to `FirmwarePipeline.Variant`,
`VPhoneCLI.PatchFirmwareCLI.VariantOption`, and
`VPhoneVirtualMachine.Variant`. Every `switch variant` block in
`FirmwarePipeline.buildComponentList` includes the new case.
- Adds `make fw_patch_exp` + `make cfw_install_exp` Makefile targets
and an `EXP=1` flag for `make setup_machine`, mutually exclusive with
`JB=1` / `DEV=1` / `LESS=1`. `SPOOF_BUILD` env var is threaded
through for later use by the build-version step.
- Adds `--exp` to `scripts/setup_machine.sh` alongside `--jb` /
`--dev` / `--less`. The post-install JB-Finalize block also fires
for EXP (since EXP inherits the JB rootfs deployment).
- Adds `scripts/cfw_install_exp.sh` as the EXP install script
starting point: identical phase set to `cfw_install_jb.sh` (JB-1..JB-5)
with banner/header/footer updated for EXP. Subsequent commits in this
branch add the EXP-only experimental phases on top.
- Updates the variants table in `AGENTS.md` / `README.md` and the
three translated READMEs to include the new `Experimental (EXP)`
row, plus a paragraph describing what EXP adds on top of JB.
Kernel patch
------------
- New `KernelEXPPatcher` orchestrator in
`sources/FirmwarePatcher/Kernel/`, chained after `KernelPatcher` +
`KernelJBPatcher` for the `.exp` variant only. Inherits
`KernelJBPatcherBase` to reuse the JB symbol-table / ADRP-BL index /
branch-encoder infrastructure.
- New `KernelEXPPatchHvVmmRename` patch in
`sources/FirmwarePatcher/Kernel/EXPPatches/`. Part A flips byte 0 of
the NUL-delimited `\\0hv_vmm_present\\0` cstring (the sysctl OID's
`oid_name` value) — `'h'` -> `'X'` — so the kernel resolves
`sysctlbyname("kern.hv_vmm_present")` as ENOENT and
`sysctlbyname("kern.Xv_vmm_present")` to the OID's real int value.
Part B mangles byte 5 of every kernel-internal occurrence of
`kern.hv_vmm_present` so callers keep hitting the renamed OID; two
byte-aligned forms are covered (NUL-delimited cstring + sandbox-profile
name-token with trailing `\\x0f`).
- Patch IDs are `kernelcache_exp.hv_vmm_oid_rename` and
`kernelcache_exp.hv_vmm_internal_caller_mangle`. Idempotent.
- `KernelJBPatcher` is unchanged at the call-site level (no
`patchHvVmmRename` call); its docstring is updated to point at
`KernelEXPPatcher` for the EXP-only patch.
JB and other variants are NOT affected: `cfw_install_jb.sh` and
`cfw_install_dev.sh` are untouched in this commit.
Add rpc-project rpcserver_ios LaunchDaemon to CFW install, update
cfw_input.tar.zst with the plist, and add iproxy 5910 + rpcclient
to README.
Co-authored-by: zqxwce <[email protected]>
Update vphoned build step to compile all split source files instead of
just vphoned.m. Tracks all .m files for staleness checking and passes
them to clang with the correct -I include path.
Co-authored-by: zqxwce <[email protected]>