Files
vphone-cli/VPhoneExecutable/VPhoneCommand/FirmwarePatcher/Kernel/JailbreakPatches/Storage/KernelJailbreakPatchDiskImages2.swift
T
LakrandClaude Opus 5.5 ae5e4531a5 Name every patch {component}-{effect}-{name}
The 117 bundled patch identifiers had grown five naming schemes
(kernel.x, jb.x, kernelcache_jb.x, txm_dev.x, bare names). Each one is now
{component}-{effect}-{name}:

- component: avpbooter, ibss, ibec, llb, txm, kernel, devicetree, dyld,
  preboot, or system-<binary> for a guest binary or file.
- effect: boot when the patch is boot-essential, exp when the standard
  preset leaves it off, cfw otherwise. A catalog test enforces this.
- name: snake_case, no hyphen, so the identifier splits from the right.

Record sites are now always <identifier>.<site>. The underscore-prefix
rule in covers(recordIdentifier:) and in the gate is gone: the new names
contain underscores, so kernel-boot-post_validation would otherwise have
covered kernel-boot-post_validation_unsigned. The 25 records that relied
on it (amfi_trustcache_1, launch_constraints_mov, sandbox_ext_N, ...) now
use a dot.

Old identifiers are not migrated. A VM whose PatchPlan or PatchSelection
names one must be patched again. The bundle becomes 2.2.0 and Launchpad
requires 2.2.0, so it never meets an old identifier from a bundle.

Launchpad's patch table shows Component, Effect and Name columns in place
of Identifier and Patch Set; the set moves to the detail line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:20:20 +09:00

274 lines
12 KiB
Swift

// KernelJailbreakPatchDiskImages2.swift — JB kernel patch: DiskImages2 ABI acceptance.
//
// Productionizes the known-good DiskImages2 pokes that make the 26.4 vphone600
// kernel accept the iOS-27 userland's DiskImages2 client (kernel driver ABI v9 vs
// daemon/controller ABI v11) and fix a RegisterNotificationPort off-by-one. Without
// these the iOS-27 personalized DDI never attaches (kernel:
// "DIDeviceCreatorUserClient::CreateDevice: Incompatible client: expected ABI
// version 9 actual 11"), so `pymobiledevice3 mounter auto-mount` fails at attach.
//
// Pairs with the sandbox mac_policy_ops[124] allow (KernelJailbreakPatchSandboxExtended)
// and the diskimagesiod isMountComplete→YES userland patch (cfw_install) that
// together let the attached DDI actually mount at /System/Developer.
//
// All sites are in the com.apple.driver.AppleDiskImages2 kext inside the MH_FILESET
// kernelcache. The existing JB patches scan the whole kernel text, which covers the
// kext; every anchor here is a globally-unique C++ signature / AssertMacros cstring,
// or a shape made unique by first pinning the enclosing function — so no DI2-range
// restriction is required.
//
// Layers:
// - GATE1/GATE2b (ABI reject NOP): version-robust — anchored on the C++ signature
// cstring + a unique `cmp #9 ; b.ne`. No-op-in-effect on version-matched userlands
// (ABI 9 == 9, so the b.ne isn't taken anyway).
// - GATE2 (notification-ports array + bound-check widen): the off-by-one is specific
// to the 26.4-kernel / iOS-27-userland hybrid, and its instruction shapes vary by
// kernel build. Applied ALL-OR-NOTHING (widening the bound checks without also
// widening the backing array would let RegisterNotificationPort write past the
// array); on builds whose codegen doesn't present all three sites the whole gate
// is skipped (logged) — the ABI gates above remain the essential attach fix.
//
// Guardrails: no hardcoded offsets/VAs/bytes in patch logic — matching is from
// Capstone decode, replacement bytes from the Keystone-backed ARM64/ARM64Encoder
// helpers.
import Foundation
import VPhonePatchKit
extension KernelJailbreakPatcher {
/// Apply all DiskImages2 ABI pokes. Wired into KernelJailbreakPatcher.findAll().
@discardableResult
func patchDiskImages2ClientAbi() -> Bool {
var ok = true
ok = patchDiskImages2CreateDeviceAbi() && ok
ok = patchDiskImages2ConnectAbi() && ok
ok = patchDiskImages2NotificationPortArray() && ok
return ok
}
// MARK: - GATE 1 / GATE 2b: ABI-version reject b.ne → NOP
/// `DIDeviceCreatorUserClient::CreateDevice` rejects a client whose controller ABI
/// (`cmp wN,#9 ; b.ne <reject>`) != 9. NOP the b.ne so the ABI-11 iOS-27 client is
/// accepted. Function pinned by its unique C++ signature cstring.
@discardableResult
func patchDiskImages2CreateDeviceAbi() -> Bool {
log("\n[JB] DiskImages2 GATE1: CreateDevice controller-ABI b.ne -> nop")
return nopAbiVersionGate(
funcSig: "static IOReturn DIDeviceCreatorUserClient::CreateDevice(OSObject *, void *, IOExternalMethodArguments *)",
patchID: "kernel-boot-di2.createdevice_abi",
desc: "nop [DI2 CreateDevice controller-ABI cmp#9/b.ne gate]",
)
}
/// `DIDeviceIOUserClient::Connect` rejects a client whose daemon ABI
/// (`cmp wN,#9 ; b.ne <reject>`) != 9. NOP the b.ne. Same shape, different function.
@discardableResult
func patchDiskImages2ConnectAbi() -> Bool {
log("\n[JB] DiskImages2 GATE2b: Connect daemon-ABI b.ne -> nop")
return nopAbiVersionGate(
funcSig: "static IOReturn DIDeviceIOUserClient::Connect(OSObject *, void *, IOExternalMethodArguments *)",
patchID: "kernel-boot-di2.connect_abi",
desc: "nop [DI2 Connect daemon-ABI cmp#9/b.ne gate]",
)
}
/// Pin the function via its unique signature cstring, then NOP the unique
/// `cmp wN,#9 ; b.ne` inside it.
private func nopAbiVersionGate(funcSig: String, patchID: String, desc: String) -> Bool {
guard let sigOff = buffer.findString(funcSig) else {
log(" [-] signature string not found: \(funcSig.prefix(48))…")
return false
}
let refs = findStringRefs(sigOff)
guard let ref = refs.first, let funcStart = findFunctionStart(ref.adrpOff) else {
log(" [-] no xref/function for signature string")
return false
}
let funcEnd = findFuncEnd(funcStart, maxSize: 0x2000)
// CMP Wn,#9 (== SUBS WZR,Wn,#9) immediately followed by B.NE.
var hits: [Int] = []
var off = funcStart
while off + 8 <= funcEnd {
defer { off += 4 }
guard let cmp = disasAt(off), cmp.mnemonic == "cmp",
let ops = cmp.detail?.operands, ops.count == 2,
ops[0].type == .register,
disasm.firstRegisterName(cmp)?.hasPrefix("w") ?? false,
ops[1].type == .immediate, ops[1].imm == 9
else { continue }
guard let nxt = disasAt(off + 4), nxt.mnemonic == "b.ne" else { continue }
hits.append(off + 4) // the b.ne
}
guard hits.count == 1 else {
log(" [-] expected 1 cmp#9/b.ne gate, found \(hits.count)")
return false
}
let bneOff = hits[0]
emit(bneOff, ARM64.nop, patchID: patchID, virtualAddress: fileOffsetToVA(bneOff), description: desc)
return true
}
// MARK: - GATE 2 (a/b/c): notification-ports array + bound checks widen
/// Widen the notification-ports array allocation AND both bound-check fields to
/// 0x800 entries, fixing the RegisterNotificationPort off-by-one (userland
/// registers at index == maxPorts, one past the array).
///
/// ALL-OR-NOTHING: widening the bound checks (type < 0x800) without also widening
/// the backing array would let RegisterNotificationPort write past the array
/// (memory corruption). So all three sites are located first and patched only if
/// all are present. On kernel builds whose notif-port codegen differs (the
/// off-by-one is specific to the 26.4-kernel / iOS-27-userland hybrid), the whole
/// gate is skipped — the version-robust ABI gates (GATE1/GATE2b) are the essential
/// attach fix.
@discardableResult
func patchDiskImages2NotificationPortArray() -> Bool {
log("\n[JB] DiskImages2 GATE2: widen notification-ports array + bound checks")
guard let allocSite = findDI2AllocPortsSizeSite() else {
log(" [~] AllocPortsArray size-shift not present on this kernel — skipping GATE2 (build-specific notif-port codegen; GATE1/GATE2b are the essential fix)")
return true
}
guard let (rnpStart, rnpEnd) = findDI2RegisterNotifFunc(),
let f1 = findUniqueFieldLoad(
funcStart: rnpStart,
funcEnd: rnpEnd,
mnemonic: "ldrh",
disp: 0xD8,
requireWDest: false,
),
let f2 = findUniqueFieldLoad(
funcStart: rnpStart,
funcEnd: rnpEnd,
mnemonic: "ldr",
disp: 0xE8,
requireWDest: true,
)
else {
log(" [~] notification-port bound-check loads not both present — skipping GATE2 (all-or-nothing)")
return true
}
// All three located — apply together.
var ok = applyDI2AllocPortsSize(at: allocSite)
ok = applyFieldLoadMov800(
at: f1,
patchID: "kernel-boot-di2.notif_boundcheck_d8",
desc: "mov wD,#0x800 [DI2 RegisterNotificationPort bound-check field1 @+0xd8]",
) && ok
ok = applyFieldLoadMov800(
at: f2,
patchID: "kernel-boot-di2.notif_boundcheck_e8",
desc: "mov wD,#0x800 [DI2 RegisterNotificationPort bound-check field2 @+0xe8]",
) && ok
return ok
}
/// Locate the AllocPortsArray allocator size arg `lsl x1, xN, #3` (count << 3 ==
/// count * 8), unique within the function. Function pinned by its C++ signature.
private func findDI2AllocPortsSizeSite() -> Int? {
guard let sigOff = buffer.findString(
"static IOReturn DIDeviceIOUserClient::AllocPortsArray(OSObject *, void *, IOExternalMethodArguments *)",
) else { return nil }
let refs = findStringRefs(sigOff)
guard let ref = refs.first, let funcStart = findFunctionStart(ref.adrpOff) else { return nil }
let funcEnd = findFuncEnd(funcStart, maxSize: 0x1000)
// The allocator size arg `lsl x1, xN, #3` (count << 3 == count * 8). Capstone on
// this toolchain decodes the lsl-immediate (a UBFM alias) as 2 operands (not the
// xd,xn,#imm 3-operand shape), so match on mnemonic + destination x1 — the only
// lsl that writes the size register, unique within AllocPortsArray. Replacement is
// a fixed `mov x1,#0x4000`, so the original shift amount is irrelevant.
var hits: [Int] = []
var off = funcStart
while off + 4 <= funcEnd {
defer { off += 4 }
guard let lsl = disasAt(off), lsl.mnemonic == "lsl",
disasm.firstRegisterName(lsl) == "x1"
else { continue }
hits.append(off)
}
return hits.count == 1 ? hits[0] : nil
}
/// Pin the RegisterNotificationPort function via its unique AssertMacros cstring.
private func findDI2RegisterNotifFunc() -> (Int, Int)? {
guard let sOff = buffer.findString("!notification_ports[ type ]") else { return nil }
let refs = findStringRefs(sOff)
guard let ref = refs.first, let funcStart = findFunctionStart(ref.adrpOff) else { return nil }
return (funcStart, findFuncEnd(funcStart, maxSize: 0x400))
}
/// Locate a unique `<mnemonic> wD,[xB,#disp]` field load in [funcStart,funcEnd).
private func findUniqueFieldLoad(
funcStart: Int,
funcEnd: Int,
mnemonic: String,
disp: Int64,
requireWDest: Bool,
) -> Int? {
var hits: [Int] = []
var off = funcStart
while off + 4 <= funcEnd {
defer { off += 4 }
guard let ins = disasAt(off), ins.mnemonic == mnemonic,
let ops = ins.detail?.operands, ops.count == 2,
ops[0].type == .register,
ops[1].type == .memory, ops[1].mem.disp == disp
else { continue }
if requireWDest, !(disasm.firstRegisterName(ins)?.hasPrefix("w") ?? false) {
continue
}
hits.append(off)
}
return hits.count == 1 ? hits[0] : nil
}
/// Rewrite the AllocPortsArray size shift to `mov x1,#0x4000` (0x800 entries * 8).
private func applyDI2AllocPortsSize(at lslOff: Int) -> Bool {
guard let name = disasm.firstRegisterName(disasAt(lslOff)!),
let dst = xRegIndex(name),
let bytes = ARM64Encoder.encodeMovzX(rd: dst, imm16: 0x4000, shift: 0)
else { log(" [-] could not encode mov x1,#0x4000"); return false }
emit(
lslOff,
bytes,
patchID: "kernel-boot-di2.allocports_size",
virtualAddress: fileOffsetToVA(lslOff),
description: "mov x1,#0x4000 [DI2 AllocPortsArray widen notif-ports alloc to 0x800 entries]",
)
return true
}
/// Rewrite a bound-check field load to `mov wD,#0x800`, deriving wD from the decode.
private func applyFieldLoadMov800(at ldOff: Int, patchID: String, desc: String) -> Bool {
guard let name = disasm.firstRegisterName(disasAt(ldOff)!),
let dst = wRegIndex(name),
let bytes = ARM64Encoder.encodeMovzW(rd: dst, imm16: 0x800, shift: 0)
else { log(" [-] could not encode mov wD,#0x800"); return false }
emit(ldOff, bytes, patchID: patchID, virtualAddress: fileOffsetToVA(ldOff), description: desc)
return true
}
// MARK: - Register-name → index helpers
private func wRegIndex(_ name: String) -> UInt32? {
if name == "wzr" {
return 31
}
guard name.hasPrefix("w"), let n = UInt32(name.dropFirst()), n < 31 else { return nil }
return n
}
private func xRegIndex(_ name: String) -> UInt32? {
if name == "xzr" {
return 31
}
guard name.hasPrefix("x"), let n = UInt32(name.dropFirst()), n < 31 else { return nil }
return n
}
}