devicetree: Add 8 identity-rewrite properties on EXP variant at fw_patch time

Splits `DeviceTreePatcher`'s property-patch list into two arrays:

  - `basePropertyPatches` (4 entries: `serial-number`,
    `home-button-type`, `artwork-device-subtype`,
    `island-notch-location`) — applied for every variant. Behaviour
    identical to pre-split.

  - `identityPropertyPatches` (8 entries — Tier 1b + 1c userland-facing
    identity surfaces) — applied only when `includeIdentityPatches` is
    true, which `FirmwarePipeline` sets exactly when `variant == .exp`.

The 8 EXP-only identity properties flip userland-visible identity toward
D47AP / iPhone17,3:

  - Tier 1b (5 properties, slot-length-preserving rewrites):
      device-tree.target-sub-type:        VPHONE600AP -> D47AP
      device-tree.compatible[1]:          iPhone99,11 -> iPhone17,3
                                           (reorder, VPHONE600AP kept first
                                            so IOKit's AppleVMApple1IO bind
                                            still resolves)
      device-tree/product.fdr-product-type: iPhone99,11 -> iPhone17,3
      device-tree/product.sub-product-type: iPhone99,11 -> iPhone17,3
      device-tree/product.unique-model:     VPHONE600AP -> D47AP

  - Tier 1c (3 properties — IOKit secondary matchers + Gestalt subtree
    rename, matched against the real D47AP DT):
      device-tree/arm-io.device_type:      vresearch1-io -> t8140-io
      device-tree/arm-io.soc-generation:   VResearch1    -> H17
      device-tree/product/vphone600-gestalt-variants.name (node rename):
                                            vphone600-gestalt-variants
                                            -> d47-gestalt-variants

Root `model` and root `target-type` are deliberately NOT in this list —
both have been empirically shown to break restore (signed-identity
cross-check in `restored_external`). Those edits run post-restore in a
later commit as EXP-JB-6.

- `sources/FirmwarePatcher/DeviceTree/DeviceTreePatcher.swift` — adds
  `includeIdentityPatches: Bool = false` to init (backwards-compatible
  default), stores it, splits `propertyPatches` into two static lists,
  iterates base first and then optionally identity in `applyPatches`.
- `sources/FirmwarePatcher/Pipeline/FirmwarePipeline.swift` — passes
  `includeIdentityPatches: variant == .exp` into the DT factory.

JB and other variants (regular, dev, less) leave the device's identity
properties untouched.
This commit is contained in:
zqxwce
2026-05-18 16:14:55 +03:00
committed by zqxwce
parent 674a86bfd4
commit f22ed3e4bf
2 changed files with 232 additions and 6 deletions
@@ -15,6 +15,12 @@ public final class DeviceTreePatcher: Patcher {
public let component = "devicetree"
public let verbose: Bool
/// Whether to apply the 8 identity-rewrite property patches (Tier 1b + 1c)
/// that flip device identity towards iPhone17,3 / D47AP. Enabled only for
/// the `.exp` variant; all other variants run the base 4 patches only so
/// they remain unaffected by the experimental identity rewrites.
let includeIdentityPatches: Bool
let buffer: BinaryBuffer
var patches: [PatchRecord] = []
var rebuiltData: Data?
@@ -36,10 +42,48 @@ public final class DeviceTreePatcher: Patcher {
enum PropertyValue {
case string(String)
case integer(UInt64)
/// Raw bytes used when the property holds a multi-string blob
/// (NUL-delimited cstrings packed back-to-back, e.g. `compatible`)
/// where Swift String escaping of embedded NULs is awkward.
case bytes(Data)
}
/// Fixed set of device tree patches, matching scripts/dtree.py PATCHES.
static let propertyPatches: [PropertyPatch] = [
/// Multi-string `compatible` blob used by patch #3 (root `compatible`).
///
/// Original layout (48 bytes):
/// "VPHONE600AP\0" "iPhone99,11\0" "AppleVirtualPlatformARM\0"
/// 11 + 1 11 + 1 23 + 1 = 48
///
/// Patched layout (48 bytes, surgical change of the middle string only):
/// "VPHONE600AP\0" "iPhone17,3\0" "AppleVirtualPlatformARM\0\0"
/// 11 + 1 10 + 1 23 + 2 = 48
///
/// `VPHONE600AP` stays as the FIRST entry so IOKit's platform-expert
/// matching at boot still binds against the kext that claims it. The
/// SECOND entry, which userland walks of the compatible list see when
/// iterating to enumerate alternate identifiers, is flipped to
/// `iPhone17,3`. The trailing `AppleVirtualPlatformARM` shifts one byte
/// earlier (now starts at byte 23 instead of 24), but every consumer of
/// `compatible` walks by NUL-terminator none depend on a fixed byte
/// offset within the blob so the shift is harmless.
static let compatibleRewrite: Data = {
var d = Data()
d.append(contentsOf: Array("VPHONE600AP".utf8))
d.append(0)
d.append(contentsOf: Array("iPhone17,3".utf8))
d.append(0)
d.append(contentsOf: Array("AppleVirtualPlatformARM".utf8))
d.append(0)
// 11+1 + 10+1 + 23+1 = 47 bytes so far; pad with one NUL to 48.
d.append(0)
return d
}()
/// Base device-tree property patches, applied for every variant.
/// Matches the pre-experimental set (serial-number, home-button-type,
/// artwork-device-subtype, island-notch-location) inherited from
/// scripts/dtree.py PATCHES.
static let basePropertyPatches: [PropertyPatch] = [
PropertyPatch(
nodePath: ["device-tree"],
property: "serial-number",
@@ -78,6 +122,157 @@ public final class DeviceTreePatcher: Patcher {
),
]
/// Experimental identity-rewrite property patches. Applied only when
/// `includeIdentityPatches` is true currently set only by the `.exp`
/// firmware variant. Other variants (regular, dev, jb, less) skip these.
///
/// Risk categories:
/// - LOW (#3 compatible[1], #11 sub-product-type, #12 unique-model):
/// read by userland identity APIs; not in the restore-signed path.
/// - HIGHER (#2 target-sub-type, #10 fdr-product-type):
/// same family as `target-type` (which broke restore in a prior
/// attempt) and FDR-related. If restore fails after a build that
/// enables these, remove just those two and retry.
/// - MEDIUM (#6 arm-io device_type, #7 arm-io soc-generation):
/// IOKit secondary matchers; the real iPhone17,3 DT carries these
/// exact values so they match the genuine D47AP.
/// - LOW-MEDIUM (#13 gestalt-variants rename): some MG-equivalent
/// code may look up the subtree by literal node name.
/// Patches for root `model` and root `target-type` are deliberately
/// NOT included here both were tried, both broke restore. They are
/// applied post-restore by EXP-JB-6 (`cfw_patch_post_restore_dt.py`)
/// in the EXP install pipeline.
static let identityPropertyPatches: [PropertyPatch] = [
// Identity rewrite (Tier 1b)
// 5 properties from the 13-entry DT inventory chosen as
// userland-facing identity surfaces. NONE of root `model` or root
// `target-type` are included both already proven to break restore.
// #2 root `target-sub-type` "VPHONE600AP" -> "D47AP".
// RISK: HIGHER. Same family as `target-type`; if restore fails
// after enabling this, remove this entry first.
PropertyPatch(
nodePath: ["device-tree"],
property: "target-sub-type",
length: 12,
flags: 0,
value: .string("D47AP"),
patchID: "devicetree.target_sub_type",
description: "Set target-sub-type to D47AP (was VPHONE600AP)"
),
// #3 root `compatible` surgical mangle. Keep VPHONE600AP as first
// entry (platform-expert binding intact), rewrite iPhone99,11 (the
// secondary entry) to iPhone17,3, keep AppleVirtualPlatformARM as
// third entry. See `compatibleRewrite` above for byte layout.
// RISK: LOW. Iterators of compatible[] read by userland will pick
// up the new identity; the kernel's platform-expert bind still
// matches the first entry, so boot is unaffected.
PropertyPatch(
nodePath: ["device-tree"],
property: "compatible",
length: 48,
flags: 0,
value: .bytes(compatibleRewrite),
patchID: "devicetree.compatible_secondary",
description: "Surgical rewrite of compatible[1]: iPhone99,11 -> iPhone17,3"
),
// #10 device-tree/product/fdr-product-type "iPhone99,11" -> "iPhone17,3".
// RISK: HIGHER. FDR = Factory Data Restore; some restore-time code
// reads this field. If restore breaks, remove this entry.
PropertyPatch(
nodePath: ["device-tree", "product"],
property: "fdr-product-type",
length: 12,
flags: 0,
value: .string("iPhone17,3"),
patchID: "devicetree.product.fdr_product_type",
description: "Set product/fdr-product-type to iPhone17,3 (was iPhone99,11)"
),
// #11 device-tree/product/sub-product-type "iPhone99,11" -> "iPhone17,3".
// RISK: LOW. Read by userland classification code; not in
// restore-signed path.
PropertyPatch(
nodePath: ["device-tree", "product"],
property: "sub-product-type",
length: 12,
flags: 0,
value: .string("iPhone17,3"),
patchID: "devicetree.product.sub_product_type",
description: "Set product/sub-product-type to iPhone17,3 (was iPhone99,11)"
),
// #12 device-tree/product/unique-model "VPHONE600AP" -> "D47AP".
// RISK: LOW. Read by libMobileGestalt and "unique device class"
// queries; not in restore-signed path.
PropertyPatch(
nodePath: ["device-tree", "product"],
property: "unique-model",
length: 12,
flags: 0,
value: .string("D47AP"),
patchID: "devicetree.product.unique_model",
description: "Set product/unique-model to D47AP (was VPHONE600AP)"
),
// Identity rewrite (Tier 1c)
// Three more candidates from the inventory that haven't been
// empirically shown to break restore. Each may still affect kernel
// boot if a kext relies on the specific value.
// #6 device-tree/arm-io/device_type "vresearch1-io" -> "t8140-io".
// RISK: MEDIUM. device_type is a secondary IOKit matcher; many
// kexts only use compatible[] for binding, but some require both.
// The actual d47ap DT carries "t8140-io" here, so this is the
// genuine iPhone17,3 value (not a fabricated one).
PropertyPatch(
nodePath: ["device-tree", "arm-io"],
property: "device_type",
length: 14,
flags: 0,
value: .string("t8140-io"),
patchID: "devicetree.arm_io.device_type",
description: "Set arm-io/device_type to t8140-io (was vresearch1-io)"
),
// #7 device-tree/arm-io/soc-generation "VResearch1" -> "H17".
// RISK: MEDIUM-LOW. soc-generation is typically a capability /
// SoC-family descriptor read by kexts to select code paths.
// d47ap (iPhone17,3) uses "H17" so we match that exactly.
PropertyPatch(
nodePath: ["device-tree", "arm-io"],
property: "soc-generation",
length: 11,
flags: 0,
value: .string("H17"),
patchID: "devicetree.arm_io.soc_generation",
description: "Set arm-io/soc-generation to H17 (was VResearch1)"
),
// #13 rename node device-tree/product/vphone600-gestalt-variants
// to "d47-gestalt-variants" by rewriting its `name` property.
// RISK: LOW-MEDIUM. Some libMobileGestalt-equivalent code may look
// up the subtree by literal node name. d47 doesn't have a
// `*-gestalt-variants` node at all (its product children are
// camera/facetime/maps/haptics/audio), so iOS handles missing
// gestalt-variants gracefully on real iPhone 17,3 devices anyway.
// Renaming should be at-worst-equivalent to that "missing node"
// path. The DTNode patcher walks by current name, so the nodePath
// here uses the OLD name; the patch rewrites the `name` property
// inside that node to the new value.
PropertyPatch(
nodePath: ["device-tree", "product", "vphone600-gestalt-variants"],
property: "name",
length: 27,
flags: 0,
value: .string("d47-gestalt-variants"),
patchID: "devicetree.product.gestalt_variants_rename",
description: "Rename node vphone600-gestalt-variants -> d47-gestalt-variants"
),
]
// MARK: - Device Tree Structures
/// A single property in a device tree node.
@@ -106,9 +301,10 @@ public final class DeviceTreePatcher: Patcher {
// MARK: - Init
public init(data: Data, verbose: Bool = true) {
public init(data: Data, verbose: Bool = true, includeIdentityPatches: Bool = false) {
buffer = BinaryBuffer(data)
self.verbose = verbose
self.includeIdentityPatches = includeIdentityPatches
}
// MARK: - Patcher
@@ -308,6 +504,18 @@ public final class DeviceTreePatcher: Patcher {
return raw
}
/// Encode raw bytes for a property whose layout the caller has prepared
/// (typically a multi-string NUL-delimited blob like `compatible`).
/// Truncates if longer than the slot, pads with NULs if shorter.
private static func encodeFixedBytes(_ data: Data, length: Int) -> Data {
if data.count > length {
return Data(data.prefix(length))
}
var out = Data(data)
out.append(contentsOf: [UInt8](repeating: 0, count: length - out.count))
return out
}
/// Encode an integer value as little-endian bytes.
private static func encodeInteger(_ value: UInt64, length: Int) throws -> Data {
var data = Data(count: length)
@@ -331,8 +539,17 @@ public final class DeviceTreePatcher: Patcher {
// MARK: - Patch Application
/// Apply all property patches and record each change.
///
/// Always runs `basePropertyPatches`. Additionally runs
/// `identityPropertyPatches` when `includeIdentityPatches` is true
/// (the `.exp` firmware variant) other variants leave the device's
/// identity properties untouched.
private func applyPatches(root: DTNode) throws {
for patch in Self.propertyPatches {
var patchesToApply = Self.basePropertyPatches
if includeIdentityPatches {
patchesToApply.append(contentsOf: Self.identityPropertyPatches)
}
for patch in patchesToApply {
let node = try resolveNode(root, path: patch.nodePath)
let prop = try findProperty(node, name: patch.property)
@@ -343,6 +560,8 @@ public final class DeviceTreePatcher: Patcher {
Self.encodeFixedString(s, length: patch.length)
case let .integer(v):
try Self.encodeInteger(v, length: patch.length)
case let .bytes(d):
Self.encodeFixedBytes(d, length: patch.length)
}
prop.length = patch.length
@@ -306,13 +306,20 @@ public final class FirmwarePipeline {
}()
))
// 7. DeviceTree same base property patches for every variant.
// 7. DeviceTree base property patches for every variant. EXP additionally
// applies the 8 identity-rewrite properties (Tier 1b + 1c) that flip the
// device's userland-visible identity toward D47AP / iPhone17,3.
let dtIncludeIdentity = variant == .exp
components.append(ComponentDescriptor(
name: "DeviceTree",
inRestoreDir: true,
searchPatterns: ["Firmware/all_flash/DeviceTree.vphone600ap.im4p"],
patcherFactories: [{ data, verbose in
DeviceTreePatcher(data: data, verbose: verbose)
DeviceTreePatcher(
data: data,
verbose: verbose,
includeIdentityPatches: dtIncludeIdentity
)
}]
))