Files
vphone-cli/sources/FirmwarePatcher/Kernel/Patches/KernelPatchApfsMount.swift
Lakr 08eb9d260f 🐦 iBSS iBEC LLB TXM
update

update
2026-03-11 04:22:50 +08:00

416 lines
15 KiB
Swift

// KernelPatchApfsMount.swift APFS mount/dev-role patches (patches 13, 14, 15, 16).
//
// Python source: scripts/patchers/kernel_patch_apfs_mount.py
// scripts/patchers/kernel_patch_apfs_graft.py (patch_handle_fsioc_graft)
import Capstone
import Foundation
extension KernelPatcher {
// MARK: - Private Helpers
/// Decode a BL instruction at `offset`. Returns the target file offset, or nil.
private func apfsMountDecodeBL(at offset: Int) -> Int? {
guard offset + 4 <= buffer.count else { return nil }
let insn = buffer.readU32(at: offset)
guard insn >> 26 == 0b100101 else { return nil }
let imm26 = insn & 0x03FF_FFFF
let signedImm = Int32(bitPattern: imm26 << 6) >> 6
return offset + Int(signedImm) * 4
}
/// Return true if the function at `funcOff` contains a RET within `maxBytes`.
private func apfsMountIsLeaf(at funcOff: Int, maxBytes: Int = 0x20) -> Bool {
let limit = min(funcOff + maxBytes, buffer.count)
var scan = funcOff
while scan + 4 <= limit {
let insn = buffer.readU32(at: scan)
if insn == ARM64.retU32 || insn == ARM64.retaaU32 || insn == ARM64.retabU32 {
return true
}
scan += 4
}
return false
}
// MARK: - Patch 13: _apfs_vfsop_mount cmp x0, x0
/// Patch 13: Replace `cmp x0, Xm` with `cmp x0, x0` in _apfs_vfsop_mount.
///
/// The target CMP follows the pattern: BL (returns current_thread in x0),
/// ADRP + LDR + LDR (load kernel_task global), CMP x0, Xm, B.EQ.
/// We require x0 as the first CMP operand to distinguish it from other CMPs.
@discardableResult
func patchApfsVfsopMountCmp() -> Bool {
log("\n[13] _apfs_vfsop_mount: cmp x0,x0 (mount rw check)")
let apfsRange = apfsTextRange()
guard let strOff = buffer.findString("apfs_mount_upgrade_checks") else {
log(" [-] 'apfs_mount_upgrade_checks' string not found")
return false
}
let refs = findStringRefs(strOff, in: apfsRange)
guard !refs.isEmpty else {
log(" [-] no code refs to apfs_mount_upgrade_checks")
return false
}
// Locate the function start of _apfs_mount_upgrade_checks.
guard let funcStart = findFunctionStart(refs[0].adrpOff) else {
log(" [-] function start not found for apfs_mount_upgrade_checks ref")
return false
}
// Gather BL callers of that function.
var callers = blIndex[funcStart] ?? []
if callers.isEmpty {
callers = blIndex[funcStart + 4] ?? []
}
if callers.isEmpty {
// Manual scan for BL callers when not in index.
for (rangeStart, rangeEnd) in codeRanges {
var off = rangeStart
while off + 4 <= rangeEnd {
if let target = apfsMountDecodeBL(at: off),
target >= funcStart, target <= funcStart + 4
{
callers.append(off)
}
off += 4
}
}
}
guard !callers.isEmpty else {
log(" [-] no BL callers of _apfs_mount_upgrade_checks found")
return false
}
for callerOff in callers {
guard callerOff >= apfsRange.start, callerOff < apfsRange.end else { continue }
let callerFuncStart = findFunctionStart(callerOff)
let scanStart = callerFuncStart ?? max(callerOff - 0x800, apfsRange.start)
let scanEnd = min(callerOff + 0x100, apfsRange.end)
var scan = scanStart
while scan + 4 <= scanEnd {
guard let insn = disasm.disassembleOne(in: buffer.data, at: scan),
insn.mnemonic == "cmp",
let detail = insn.aarch64,
detail.operands.count >= 2
else {
scan += 4; continue
}
let ops = detail.operands
// Both operands must be registers.
guard ops[0].type == AARCH64_OP_REG, ops[1].type == AARCH64_OP_REG else {
scan += 4; continue
}
// First operand must be x0 (return value from BL current_thread).
guard ops[0].reg == AARCH64_REG_X0 else {
scan += 4; continue
}
// Skip CMP x0, x0 (already patched or trivially true).
guard ops[0].reg != ops[1].reg else {
scan += 4; continue
}
let va = fileOffsetToVA(scan)
emit(
scan,
ARM64.cmpX0X0,
patchID: "kernel.apfs_vfsop_mount.cmp_x0_x0",
virtualAddress: va,
description: "cmp x0,x0 (was \(insn.mnemonic) \(insn.operandString)) [_apfs_vfsop_mount]"
)
return true
}
}
log(" [-] CMP x0,Xm not found near mount_upgrade_checks caller")
return false
}
// MARK: - Patch 14: _apfs_mount_upgrade_checks mov w0, #0
/// Patch 14: Replace `tbnz w0, #0xe, ...` with `mov w0, #0`.
///
/// Within the function a BL calls a small leaf flag-reading function, then
/// TBNZ w0, #0xe branches to the error path. Replace TBNZ with mov w0,#0.
@discardableResult
func patchApfsMountUpgradeChecks() -> Bool {
log("\n[14] _apfs_mount_upgrade_checks: mov w0,#0 (tbnz bypass)")
let apfsRange = apfsTextRange()
guard let strOff = buffer.findString("apfs_mount_upgrade_checks") else {
log(" [-] 'apfs_mount_upgrade_checks' string not found")
return false
}
let refs = findStringRefs(strOff, in: apfsRange)
guard !refs.isEmpty else {
log(" [-] no code refs to apfs_mount_upgrade_checks")
return false
}
guard let funcStart = findFunctionStart(refs[0].adrpOff) else {
log(" [-] function start not found")
return false
}
let limit = min(funcStart + 0x200, buffer.count)
var scan = funcStart
while scan + 4 <= limit {
// Stop at PACIBSP (new function boundary), but not at early returns.
if scan > funcStart + 8, buffer.readU32(at: scan) == ARM64.pacibspU32 {
break
}
guard let blTarget = apfsMountDecodeBL(at: scan) else {
scan += 4; continue
}
// Target must be a small leaf function.
guard apfsMountIsLeaf(at: blTarget) else {
scan += 4; continue
}
// Next instruction must be TBNZ w0, #N (any bit).
let nextOff = scan + 4
guard nextOff + 4 <= buffer.count,
let nextInsn = disasm.disassembleOne(in: buffer.data, at: nextOff),
nextInsn.mnemonic == "tbnz",
let detail = nextInsn.aarch64,
!detail.operands.isEmpty,
detail.operands[0].type == AARCH64_OP_REG,
detail.operands[0].reg == AARCH64_REG_W0
else {
scan += 4; continue
}
let va = fileOffsetToVA(nextOff)
emit(
nextOff,
ARM64.movW0_0,
patchID: "kernel.apfs_mount_upgrade_checks.mov_w0_0",
virtualAddress: va,
description: "mov w0,#0 [_apfs_mount_upgrade_checks]"
)
return true
}
log(" [-] BL + TBNZ w0 pattern not found")
return false
}
// MARK: - Patch 15: _handle_fsioc_graft mov w0, #0
/// Patch 15: Replace the BL to `validate_payload_and_manifest` with `mov w0, #0`
/// inside `_handle_fsioc_graft`.
@discardableResult
func patchHandleFsiocGraft() -> Bool {
log("\n[15] _handle_fsioc_graft: mov w0,#0 (validate BL)")
let apfsRange = apfsTextRange()
// Locate "handle_fsioc_graft" string (expect surrounding NUL bytes).
guard let raw = "handle_fsioc_graft".data(using: .utf8) else { return false }
var searchPattern = Data([0x00])
searchPattern.append(raw)
searchPattern.append(0x00)
guard let patternRange = buffer.data.range(of: searchPattern) else {
log(" [-] 'handle_fsioc_graft' string not found")
return false
}
let fsiocStrOff = patternRange.lowerBound + 1 // skip leading NUL
let fsiocRefs = findStringRefs(fsiocStrOff, in: apfsRange)
guard !fsiocRefs.isEmpty else {
log(" [-] no code refs to handle_fsioc_graft string")
return false
}
guard let fsiocStart = findFunctionStart(fsiocRefs[0].adrpOff) else {
log(" [-] _handle_fsioc_graft function start not found")
return false
}
// Locate validate_payload_and_manifest function start.
guard let valStrOff = buffer.findString("validate_payload_and_manifest") else {
log(" [-] 'validate_payload_and_manifest' string not found")
return false
}
let valRefs = findStringRefs(valStrOff, in: apfsRange)
guard !valRefs.isEmpty else {
log(" [-] no code refs to validate_payload_and_manifest")
return false
}
guard let valFunc = findFunctionStart(valRefs[0].adrpOff) else {
log(" [-] validate_payload_and_manifest function start not found")
return false
}
// Scan _handle_fsioc_graft for BL targeting valFunc.
let scanEnd = min(fsiocStart + 0x400, buffer.count)
var scan = fsiocStart
while scan + 4 <= scanEnd {
if scan > fsiocStart + 8, buffer.readU32(at: scan) == ARM64.pacibspU32 {
break
}
if isBL(at: scan, target: valFunc) {
let va = fileOffsetToVA(scan)
emit(
scan,
ARM64.movW0_0,
patchID: "kernel.handle_fsioc_graft.mov_w0_0",
virtualAddress: va,
description: "mov w0,#0 [_handle_fsioc_graft]"
)
return true
}
scan += 4
}
log(" [-] BL to validate_payload_and_manifest not found in _handle_fsioc_graft")
return false
}
// MARK: - Patch 16: handle_get_dev_by_role bypass entitlement gate
/// Patch 16: NOP CBZ/CBNZ on X0/W0 that branch to entitlement-error blocks
/// in `handle_get_dev_by_role`.
///
/// Error blocks are identified by `mov w8, #0x332D` or `mov w8, #0x333B`
/// within the first 0x30 bytes (known entitlement-gate line IDs).
@discardableResult
func patchHandleGetDevByRoleEntitlement() -> Bool {
log("\n[16] handle_get_dev_by_role: bypass entitlement gate")
let apfsRange = apfsTextRange()
guard let strOff = buffer.findString("com.apple.apfs.get-dev-by-role") else {
log(" [-] entitlement string not found")
return false
}
let refs = findStringRefs(strOff, in: apfsRange)
guard !refs.isEmpty else {
log(" [-] no code refs to entitlement string")
return false
}
for ref in refs {
guard let funcStart = findFunctionStart(ref.adrpOff) else { continue }
let funcEnd = min(funcStart + 0x1200, buffer.count)
var candidates: [(off: Int, target: Int)] = []
var scan = funcStart
while scan + 4 <= funcEnd {
guard let insn = disasm.disassembleOne(in: buffer.data, at: scan),
insn.mnemonic == "cbz" || insn.mnemonic == "cbnz",
let detail = insn.aarch64,
detail.operands.count >= 2
else {
scan += 4; continue
}
let ops = detail.operands
guard ops[0].type == AARCH64_OP_REG,
ops[1].type == AARCH64_OP_IMM
else {
scan += 4; continue
}
let reg = ops[0].reg
guard reg == AARCH64_REG_X0 || reg == AARCH64_REG_W0 else {
scan += 4; continue
}
let target = Int(ops[1].imm)
guard target > scan, target >= funcStart, target < funcEnd else {
scan += 4; continue
}
if isEntitlementErrorBlock(at: target, funcEnd: funcEnd) {
if !candidates.contains(where: { $0.off == scan }) {
candidates.append((off: scan, target: target))
}
}
scan += 4
}
if !candidates.isEmpty {
for cand in candidates {
let va = fileOffsetToVA(cand.off)
emit(
cand.off,
ARM64.nop,
patchID: "kernel.handle_get_dev_by_role.gate_\(String(format: "%X", cand.off))",
virtualAddress: va,
description: "NOP [handle_get_dev_by_role entitlement gate -> 0x\(String(format: "%X", cand.target))]"
)
}
return true
}
}
log(" [-] handle_get_dev_by_role entitlement gate pattern not found")
return false
}
/// Return true if the block at `targetOff` contains `mov w8, #0x332D` or
/// `mov w8, #0x333B` within the first 0x30 bytes (entitlement-gate line IDs).
private func isEntitlementErrorBlock(at targetOff: Int, funcEnd: Int) -> Bool {
let scanEnd = min(targetOff + 0x30, funcEnd)
var off = targetOff
while off + 4 <= scanEnd {
guard let insn = disasm.disassembleOne(in: buffer.data, at: off),
let detail = insn.aarch64
else {
off += 4; continue
}
// Stop on call, unconditional branch, or return different path.
if insn.mnemonic == "bl" || insn.mnemonic == "b"
|| insn.mnemonic == "ret" || insn.mnemonic == "retab"
{
break
}
if insn.mnemonic == "mov", detail.operands.count >= 2 {
let ops = detail.operands
if ops[0].type == AARCH64_OP_REG,
ops[0].reg == AARCH64_REG_W8,
ops[1].type == AARCH64_OP_IMM,
ops[1].imm == 0x332D || ops[1].imm == 0x333B
{
return true
}
}
off += 4
}
return false
}
// MARK: - Aggregate entry point
/// Apply all APFS mount patches (13, 14, 15, 16).
@discardableResult
func patchApfsMount() -> Bool {
let r13 = patchApfsVfsopMountCmp()
let r14 = patchApfsMountUpgradeChecks()
let r15 = patchHandleFsiocGraft()
let r16 = patchHandleGetDevByRoleEntitlement()
return r13 && r14 && r15 && r16
}
}