Files
vphone-cli/VPhoneExecutable/VPhoneCommand/FirmwarePatcher/Kernel/KernelJailbreakPatcher.swift
T
LakrandClaude Opus 5 30c8512c89 Declare every patch in a patch set and select it with a preset
Patches were scattered across the patchers that applied them: nothing listed
what a run would do, and nothing could turn one off. This adds the manifest
layer that names them and the preset layer that chooses.

VPhonePatchKit is a distribution framework (library evolution on) holding the
model: VPhoneVersion, VPhoneVersionRequirement, VPhonePatchDeclaration,
VPhonePatchSetManifest, VPhonePatchPreset, VPhonePatchPlan and the gate a
patcher consults before each write. Capstone and the ARM64 encoder moved in
behind an internal import, so nothing downstream sees the package.

Ten bundled sets in FirmwarePatcher/PatchSets declare every existing patch,
checked against the patchers by FirmwarePatchSetCatalogTests. Two presets ship
in VPhone.bundle: standard, and extended for the experimental sets. A
version-pinned patch is present in the manifest but off unless a preset or a
per-VM checkmark asks for it, and neither can widen its version gate.

Patch sets also load from outside the tool. A .vphonepatchset is a macOS
loadable bundle whose Contents/Resources/Manifest.plist is read before any of
its code is mapped, and whose executable exports one symbol,
vphone_patch_set_principal, returning a VPhonePatchSetPrincipal that hands the
pipeline one BufferedPatcher per component the plan enabled.

Not NSPrincipalClass, which is how a loadable bundle normally names its entry
point: library evolution makes VPhonePatchSetPrincipal a resilient superclass,
so a subclass of it needs runtime metadata initialization and is not registered
with the ObjC runtime when the image is mapped. NSClassFromString cannot find
it, and Bundle.principalClass then silently returns whichever class was
registered — the example set's patcher rather than its principal. A @_cdecl
symbol found with dlsym has none of that.

External sets are boot-chain only, because root cfw install loads no external
set; a preset that names one lives in ~/.vphone/patches_presets and cannot
shadow a shipped identifier. `vphone-cli patchset import` copies a set into
~/.vphone/patchsets and ad hoc signs it if it arrived unsigned, so a bundle
straight out of Xcode loads: the linker signs its Mach-O but seals no
resources, which codesign rejects until one pass over the bundle fixes it. The
signature is re-checked from disk at every load, and PatchSetLoaderTests proves
that over the example set — inspect, validate, tamper, load, patch, gate off.

BufferedPatcher replaces the nine concrete downcasts the pipeline used to read
patched bytes back with, which is what lets an out-of-tree patcher return any.

Launchpad gains a patch table per machine, `vphone-cli fw set-patches` writes
the selection, and Skills/authoring-patch-sets documents the whole flow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 19:58:18 +09:00

126 lines
5.7 KiB
Swift

// KernelJailbreakPatcher.swift — JB kernel patcher orchestrator.
//
// Historical note: derived from the legacy Python firmware patcher during the Swift migration.
import Foundation
import VPhonePatchKit
/// JB kernel patcher across 3 groups. Variant- and feature-gated methods can
/// change the emitted record count; iOS-27-only patches are gated by `applyIOS27`
/// and Frida Stalker relaxations by `applyFrida`, which the pipeline sets from the
/// plan — the `com.vphone.patchset.kernel.frida` patches, off in `standard`.
///
/// Group A: Core gate-bypass methods
/// Group B: Pattern/string anchored methods
/// Group C: Shellcode/trampoline heavy methods
public final class KernelJailbreakPatcher: KernelJailbreakPatcherBase, BufferedPatcher {
public let component = "kernelcache_jb"
/// Gates the iOS-27-only kernel patches. These target an iOS-27 userland running
/// on the 26.4 kernel; on a 26.x base they are unnecessary and some are actively
/// harmful (e.g. the IOMFB SwapEnd size gate would reject 26.x's native 0x588 swap
/// struct → dead display, the 26.5 regression). The pipeline sets this from the
/// iPhone base ProductVersion (false for 18.x/26.x → byte-identical to pre-branch);
/// standalone patch-component defaults it true so the dev tool exercises the full
/// set (override with --target-os).
public var applyIOS27 = false
/// Opt-in Frida Stalker kernel relaxations. Set from the plan: `standard` blocks
/// both, `extended` and a per-VM checkmark turn them on, and their own
/// cloudOS 26.4+ gate then decides whether they land. Baseline JB/EXP firmware is
/// byte-identical when false.
public var applyFrida = false
public func findAll() throws -> [PatchRecord] {
parseMachO()
buildADRPIndex()
buildBLIndex()
buildSymbolTable()
findPanic()
// Group A
patchAmfiCdhashInTrustcache()
patchTaskConversionEvalInternal()
patchSandboxHooksExtended()
patchIoucFailedMacf()
// iOS-27-only (gated — a 26.x base skips these entirely). Both target a 27
// userland on the 26.4 kernel:
// - IOUC sandbox gate bypass: the IOKit user-client open path's Sandbox gate
// (separate from the MACF gate above) spuriously denies backboardd its
// IOMFB/IOSurface/HID user clients → no present + nil main display →
// SpringBoard crash-loop. Mirrors the MACF gate.
// - DiskImages2 DDI ABI (kernel driver v9 vs iOS-27 controller/daemon v11) +
// RegisterNotificationPort off-by-one, so the personalized DDI attaches
// (/System/Developer auto-mount). Pairs with the sandbox ops[124] allow
// and the diskimagesiod isMountComplete→YES userland patch (cfw_install).
if applyIOS27 {
patchIoucFailedSandbox()
patchDiskImages2ClientAbi()
}
// Group B
patchPostValidationAdditional()
patchProcSecurityPolicy()
patchProcPidinfo()
patchConvertPortToMap()
patchBsdInitAuth()
patchDounmount()
patchIoSecureBsdRoot()
patchLoadDylinker()
patchMacMount()
patchNvramVerifyPermission()
patchSharedRegionMap()
patchSpawnValidatePersona()
patchTaskForPid()
patchThidShouldCrash()
patchVmFaultEnterPrepare()
patchVmMapProtect()
// Opt-in Frida Stalker support, from the kernel.frida patch set:
// existing-thread follow (thread_set_state) + repeated VM_PROT_COPY
// overwrite (vm_map_delete).
if applyFrida {
patchThreadSetStateEntitlementFlag()
patchVmMapDeleteImmutableCode()
}
// Group C
patchCredLabelUpdateExecve()
patchHookCredLabelUpdateExecve()
patchKcall10()
patchSyscallmaskApplyToProc()
// iOS-27-only (gated — a 26.x base skips these entirely). All target a 27
// userland on the 26.4 kernel and are unnecessary or actively harmful on 26.x:
// - exec ip_mac_return SECURITY_POLICY kill bypass: AMFI's exec hooks reject a
// userland newer than the kernel (27 binaries' validation category) →
// ip_mac_return != 0 → core daemons die at exec → boot deadlock. 26.x
// binaries pass (ip_mac_return == 0), so it is not needed there.
// - container-manager exec-upcall force-success: iOS 27 deleted the kernel-side
// containermanagerd upcall, so on the 26.4 kernel it fails for every 27 app →
// autoboxed into temporary-sandbox → Campo/intelligencetasksd/feedbackd
// crash-loop. On 26.x the upcall succeeds, so it is not needed.
// - IOMFB SwapEnd size gates: 27's force-kern present (cfw_patch_iomfb_force_kern)
// sends a 0x6e0 SwapEnd struct; the 26.4 userclient exact-checks 0x588 in two
// places, so both gates are relaxed to accept 0x6e0. HARMFUL on 26.x — it
// sends the native 0x588, which the retargeted handler gate would then reject
// → every framebuffer swap fails → dead display (the 26.5 regression).
if applyIOS27 {
patchExecSecurityPolicyKill()
patchContainerManagerUpcall()
patchIomfbSwapEndVariableSize() // dispatch checkStructureInputSize → variable
patchIomfbSwapEndHandlerSize() // handler cmp w2,#0x588 → 0x6e0
patchFpfsScopedVnodeOpen() // ops[267] → FileProvider-scoped trampoline (fpfs respring fix)
}
return patches
}
public func apply() throws -> Int {
// `emit()` already wrote every record through to `buffer.data`.
let records = try (patches.isEmpty ? findAll() : patches)
return records.count
}
}