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]>
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.
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.
* Implement battery sync with host
* Clean up the previous sync implementation
* Enable the battery sync functionality by default
* Re-sync the VM's battery state when vphoned reconnects
* fix: prefer project venv Python for patchers
* add: VM backup, restore, and switch support
Named backups via rsync --sparse for efficient sparse disk handling.
- vm_backup.sh: save current VM as a named backup to vm.backups/
- vm_restore.sh: restore a named backup into vm/
- vm_switch.sh: save current + restore target in one step
- Makefile targets: vm_backup, vm_restore, vm_switch, vm_list
- Documentation added to all READMEs (EN, ZH, KO, JA)
Closes#204
Made-with: Cursor
* fix: preserve caller PATH through Nix zshenv reset in cfw scripts
Nix darwin's /etc/zshenv resets PATH on every zsh subprocess,
discarding the Makefile's carefully constructed PATH (which includes
.venv/bin and /opt/homebrew/bin). This caused 'Missing Python deps'
and ldid PKCS12_parse errors during cfw_install.
Pass the Makefile PATH through _VPHONE_PATH env var (which zshenv
won't touch), and restore it at the top of each cfw_install script.
* fix(cfw_install_dev): add python resolver, use glob for vphoned sources
- Add _resolve_python3() matching cfw_install.sh so the venv python
is used instead of Nix system python (which lacks capstone/keystone).
- Replace hardcoded VPHONED_SRCS list with glob pattern to auto-pick
up new .m files (was missing 5 files: accessibility, apps, clipboard,
settings, url — causing linker errors).
* fix: amfidont uses bundle binary CDHash and .build path
make boot launches the bundle binary (.build/vphone-cli.app/Contents/
MacOS/vphone-cli), not the release binary. amfidont's --path must
cover the .app bundle location.
- amfidont_allow_vphone depends on bundle (not build)
- start_amfidont_for_vphone.sh extracts CDHash from bundle binary
- --path points to .build/ so amfidont covers .app bundle contents
* fix(preflight): prevent run_capture errexit on non-zero return
zsh set -e is global scope — set -e inside run_capture then
return 137 triggers errexit and kills the script before reaching
the assert-bootable check. Use '|| rc=$?' instead to capture
the exit code without modifying errexit state.