Files
vphone-cli/sources/FirmwarePatcher/Kernel/JBPatches/KernelJBPatchSandboxExtended.swift
T
zqxwceandClaude Opus 4.8 9becab4031 cfw: Fix iOS 27 DDI (/System/Developer) auto-mount
Productionize the 3-part fix that makes `pymobiledevice3 mounter auto-mount`
land the personalized DDI at /System/Developer on the iOS-27-userland /
26.4-vphone600-kernel (c0ecdb4b) JB hybrid.

- KernelJBPatchDiskImages2.swift (new; wired into KernelJBPatcher.findAll):
  DiskImages2 ABI acceptance. GATE1/GATE2b NOP the CreateDevice/Connect
  cmp#9/b.ne ABI-version rejects (kernel driver v9 vs iOS-27 controller/daemon
  v11). GATE2 widens the RegisterNotificationPort backing array + both
  bound-check field loads (all-or-nothing) to clear the `type < getMaxPorts`
  off-by-one that otherwise fails the attach ("Can't register notification
  port").
- KernelJBPatchSandboxExtended: retarget mac_policy_ops[124]
  (mpo_proc_check_syscall_unix) to the allow stub so MobileStorageMounter's
  mount_apfs can make the mount(2) syscall (unix 167) — else the kernel
  Sandbox denies it ("Protobox: mount_apfs deny(1) syscall-unix 167").
- cfw_patch_diskimagesiod.py + cfw.py + cfw_install.sh (gated to 27.*):
  force -[DIDiskArb isMountCompleteWithExpectedCount:diskTracker:] -> YES so
  MobileStorageMounter's waitForDAMount returns and it performs the real
  nobrowse mount.

GATE2a anchor fix (the critical one): Capstone on this toolchain decodes the
AllocPortsArray size-shift lsl-immediate (a UBFM alias) as 2 operands, not the
xd,xn,#imm 3-operand shape. The original 3-operand + imm==3 match found 0, so
the all-or-nothing silently skipped GATE2 on every build (only manual dd pokes
ever worked). Now matched on mnemonic + destination x1 — the unique
size-writing lsl in AllocPortsArray; the replacement is a fixed mov x1,#0x4000
so the shift amount is irrelevant. Verified from a clean build on c0ecdb4b via
`patch-component --component kernel-jb --records-out`: all five di2 records
emit (createdevice, connect, allocports_size, notif_boundcheck_d8/e8), and
`pmd3 mounter auto-mount` -> rc=0 with the DDI mounted, no poke.

Documented in research/0_binary_patch_comparison.md (JB-09, JB-28, CFW
binary-patch #13).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013pk5tsoBeuu3jhkmnRFtic
2026-07-20 16:20:34 +03:00

219 lines
9.1 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// KernelJBPatchSandboxExtended.swift — JB kernel patch: Extended sandbox hooks bypass
//
// Historical note: derived from the legacy Python firmware patcher during the Swift migration.
//
// Strategy (ops-table retargeting — matches upstream patch_fw.py):
// 1. Locate mac_policy_conf via the "Seatbelt sandbox policy" and "Sandbox" strings
// in __DATA_CONST / __DATA segments. The conf struct at offset +32 holds a tagged
// pointer to mac_policy_ops.
// 2. Find the common Sandbox allow stub (mov x0,#0 ; ret) — the highest-address
// instance in sandbox text is the canonical one.
// 3. For each extended hook index (201316), read the 8-byte tagged pointer from
// ops_table + index * 8, retarget its low 32 bits to allow_stub while preserving
// the high 32 bits (PAC/auth-rebase metadata), and emit the new value.
import Foundation
extension KernelJBPatcher {
/// Extended sandbox hooks bypass: retarget ops entries to the allow stub.
@discardableResult
func patchSandboxHooksExtended() -> Bool {
log("\n[JB] Sandbox extended hooks: retarget ops entries to allow stub")
guard let opsTable = findSandboxOpsTableViaConf() else {
return false
}
guard let allowStub = findSandboxAllowStub() else {
log(" [-] common Sandbox allow stub not found")
return false
}
// Extended hook index table (name → ops slot index). Entries 201..316 are the
// base JB sandbox bypass and apply on every JB base; the iOS-27-only
// syscall-unix entry (124) is appended below, gated on applyIOS27.
var hookIndices: [(String, Int)] = [
("iokit_check_201", 201),
("iokit_check_202", 202),
("iokit_check_203", 203),
("iokit_check_204", 204),
("iokit_check_205", 205),
("iokit_check_206", 206),
("iokit_check_207", 207),
("iokit_check_208", 208),
("iokit_check_209", 209),
("iokit_check_210", 210),
("vnode_check_getattr", 245),
("proc_check_get_cs_info", 249),
("proc_check_set_cs_info", 250),
("proc_check_set_cs_info2", 252),
("vnode_check_chroot", 254),
("vnode_check_create", 255),
("vnode_check_deleteextattr", 256),
("vnode_check_exchangedata", 257),
("vnode_check_exec", 258),
("vnode_check_getattrlist", 259),
("vnode_check_getextattr", 260),
("vnode_check_ioctl", 261),
("vnode_check_link", 264),
("vnode_check_listextattr", 265),
("vnode_check_open", 267),
("vnode_check_readlink", 270),
("vnode_check_setattrlist", 275),
("vnode_check_setextattr", 276),
("vnode_check_setflags", 277),
("vnode_check_setmode", 278),
("vnode_check_setowner", 279),
("vnode_check_setutimes", 280),
("vnode_check_stat", 281),
("vnode_check_truncate", 282),
("vnode_check_unlink", 283),
("vnode_check_fsgetpath", 316),
]
// iOS-27-only: mpo_proc_check_syscall_unix[124] → allow lets the mount_apfs that
// MobileStorageMounter spawns make the mount(2) syscall (unix 167) for the iOS-27
// personalized DDI (/System/Developer). Gated so a 26.x base keeps its syscall-unix
// MAC filter intact (else kernel Sandbox: "Protobox: mount_apfs deny(1) syscall-unix
// 167"). Index 124 is calibrated against this kernel's mac_policy_ops layout: the
// vnode_check_open==267 / vnode_check_fsgetpath==316 entries above match the
// reference XNU struct order exactly, so mpo_proc_check_syscall_unix==124 holds.
if applyIOS27 {
hookIndices.append(("proc_check_syscall_unix", 124))
}
var patched = 0
for (hookName, idx) in hookIndices {
let entryOff = opsTable + idx * 8
guard entryOff + 8 <= buffer.count else { continue }
let entryRaw = buffer.readU64(at: entryOff)
guard entryRaw != 0 else { continue }
guard let newEntry = encodeAuthRebaseLike(origVal: entryRaw, targetOff: allowStub) else {
continue
}
var newBytes = Data(count: 8)
withUnsafeBytes(of: newEntry.littleEndian) { src in
newBytes.replaceSubrange(0 ..< 8, with: src)
}
emit(entryOff, newBytes,
patchID: "sandbox_ext_\(idx)",
virtualAddress: nil,
description: "ops[\(idx)] -> allow stub [_hook_\(hookName)]")
patched += 1
}
if patched == 0 {
log(" [-] no extended sandbox hooks retargeted")
return false
}
return true
}
// MARK: - Sandbox ops table discovery
/// Locate the Sandbox mac_policy_ops table via mac_policy_conf.
///
/// Searches __DATA_CONST and __DATA segments for the conf struct:
/// [0..7] tagged ptr → "Sandbox\0"
/// [8..15] tagged ptr → "Seatbelt sandbox policy\0"
/// [32..39] tagged ptr → mac_policy_ops table
private func findSandboxOpsTableViaConf() -> Int? {
log("\n[*] Finding Sandbox mac_policy_ops via mac_policy_conf...")
guard let seatbeltOff = buffer.findString("Seatbelt sandbox policy") else {
log(" [-] Sandbox/Seatbelt strings not found")
return nil
}
// Find "\0Sandbox\0" and return offset of 'S'
guard let sandboxPattern = "\u{0}Sandbox\u{0}".data(using: .utf8),
let sandboxRange = buffer.data.range(of: sandboxPattern)
else {
log(" [-] Sandbox string not found")
return nil
}
let sandboxOff = sandboxRange.lowerBound + 1 // skip leading NUL
log(" [*] Sandbox string at foff 0x\(String(format: "%X", sandboxOff)), Seatbelt at 0x\(String(format: "%X", seatbeltOff))")
// Collect __DATA_CONST and __DATA segment ranges.
var dataRanges: [(Int, Int)] = []
for seg in segments {
if seg.name == "__DATA_CONST" || seg.name == "__DATA", seg.fileSize > 0 {
let s = Int(seg.fileOffset)
dataRanges.append((s, s + Int(seg.fileSize)))
}
}
for (dStart, dEnd) in dataRanges {
var i = dStart
while i <= dEnd - 40 {
defer { i += 8 }
let val = buffer.readU64(at: i)
// Must not be zero or a tagged (high-bit set) pointer at position [0].
if val == 0 || (val & (1 << 63)) != 0 { continue }
// Low 43 bits must point to sandboxOff (auth-rebase chained ptr format).
guard (val & 0x7FF_FFFF_FFFF) == UInt64(sandboxOff) else { continue }
let val2 = buffer.readU64(at: i + 8)
if (val2 & (1 << 63)) != 0 { continue }
guard (val2 & 0x7FF_FFFF_FFFF) == UInt64(seatbeltOff) else { continue }
// Offset +32: tagged ptr to mac_policy_ops.
let valOps = buffer.readU64(at: i + 32)
if (valOps & (1 << 63)) == 0 {
let opsOff = Int(valOps & 0x7FF_FFFF_FFFF)
log(" [+] mac_policy_conf at foff 0x\(String(format: "%X", i)), mpc_ops -> 0x\(String(format: "%X", opsOff))")
return opsOff
}
}
}
log(" [-] mac_policy_conf not found")
return nil
}
// MARK: - Allow stub discovery
/// Find the Sandbox common allow stub: `mov x0, #0 ; ret`.
///
/// Scans sandbox kext text for consecutive MOV_X0_0 + RET pairs and returns
/// the highest-address hit (matches upstream patch_fw.py choice).
private func findSandboxAllowStub() -> Int? {
// Use the Sandbox kext's __TEXT_EXEC.__text range (matches Python self.sandbox_text).
let sbRange = sandboxTextRange()
let (sbStart, sbEnd) = (sbRange.start, sbRange.end)
var hits: [Int] = []
var off = sbStart
while off < sbEnd - 8 {
if buffer.readU32(at: off) == ARM64.movX0_0_U32,
buffer.readU32(at: off + 4) == ARM64.retU32
{
hits.append(off)
}
off += 4
}
guard let stub = hits.max() else { return nil }
log(" [+] common Sandbox allow stub at 0x\(String(format: "%X", stub))")
return stub
}
// MARK: - Auth-rebase pointer retargeting
/// Retarget an auth-rebase chained pointer while preserving PAC metadata.
///
/// Auth-rebase format (high bit set): [63]=1, [62:32]=auth/diversity bits, [31:0]=target
/// Replace the low 32 bits with the new target offset.
private func encodeAuthRebaseLike(origVal: UInt64, targetOff: Int) -> UInt64? {
// Must be a tagged (auth) pointer — bit 63 must be set.
guard (origVal & (1 << 63)) != 0 else { return nil }
let highBits = origVal & 0xFFFF_FFFF_0000_0000
let newLow = UInt64(targetOff) & 0xFFFF_FFFF
return highBits | newLow
}
}